X Tutup
Skip to content

Commit 3217ac2

Browse files
Merge branch 'estimate-undo-time'
Conflicts: bpython/curtsiesfrontend/repl.py
2 parents b23e191 + 0811bdd commit 3217ac2

File tree

10 files changed

+136
-24
lines changed

10 files changed

+136
-24
lines changed

bpython/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,9 @@ def p_key(self, key):
878878
return ''
879879

880880
elif key in key_dispatch[config.undo_key]: # C-r
881-
self.undo()
881+
n = self.prompt_undo()
882+
if n > 0:
883+
self.undo(n=n)
882884
return ''
883885

884886
elif key in key_dispatch[config.search_key]:

bpython/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def loadini(struct, configfile):
5353
'hist_length': 100,
5454
'hist_duplicates': True,
5555
'paste_time': 0.02,
56+
'single_undo_time': 1.0,
5657
'syntax': True,
5758
'tab_length': 4,
5859
'pastebin_confirm': True,
@@ -141,6 +142,7 @@ def get_key_no_doublebind(command):
141142
struct.syntax = config.getboolean('general', 'syntax')
142143
struct.arg_spec = config.getboolean('general', 'arg_spec')
143144
struct.paste_time = config.getfloat('general', 'paste_time')
145+
struct.single_undo_time = config.getfloat('general', 'single_undo_time')
144146
struct.highlight_show_source = config.getboolean('general',
145147
'highlight_show_source')
146148
struct.hist_file = config.get('general', 'hist_file')

bpython/curtsies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def mainloop(config, locals_, banner, interp=None, paste=None, interactive=True)
8282
schedule_refresh = input_generator.scheduled_event_trigger(bpythonevents.ScheduledRefreshRequestEvent)
8383
request_reload = input_generator.threadsafe_event_trigger(bpythonevents.ReloadEvent)
8484
interrupting_refresh = input_generator.threadsafe_event_trigger(lambda: None)
85+
request_undo = input_generator.event_trigger(bpythonevents.UndoEvent)
8586

8687
def on_suspend():
8788
window.__exit__(None, None, None)
@@ -98,6 +99,7 @@ def after_suspend():
9899
request_refresh=request_refresh,
99100
schedule_refresh=schedule_refresh,
100101
request_reload=request_reload,
102+
request_undo=request_undo,
101103
get_term_hw=window.get_term_hw,
102104
get_cursor_vertical_diff=window.get_cursor_vertical_diff,
103105
banner=banner,

bpython/curtsiesfrontend/events.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ def __repr__(self):
3434

3535
class RunStartupFileEvent(curtsies.events.Event):
3636
"""Request to run the startup file."""
37+
38+
39+
class UndoEvent(curtsies.events.Event):
40+
"""Request to undo."""
41+
def __init__(self, n=1):
42+
self.n = n

bpython/curtsiesfrontend/interaction.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,14 @@ def process_event(self, e):
7474
elif isinstance(e, events.PasteEvent):
7575
for ee in e.events:
7676
self.add_normal_character(ee if len(ee) == 1 else ee[-1]) #strip control seq
77+
elif e in ['<ESC>'] or isinstance(e, events.SigIntEvent):
78+
self.request_greenlet.switch(False)
79+
self.escape()
7780
elif e in edit_keys:
7881
self.cursor_offset_in_line, self._current_line = edit_keys[e](self.cursor_offset_in_line, self._current_line)
79-
elif e == "<Ctrl-c>":
82+
elif e == "<Ctrl-c>": #TODO can this be removed?
8083
raise KeyboardInterrupt()
81-
elif e == "<Ctrl-d>":
84+
elif e == "<Ctrl-d>": #TODO this isn't a very intuitive behavior
8285
raise SystemExit()
8386
elif self.in_prompt and e in ("\n", "\r", "<Ctrl-j>", "Ctrl-m>"):
8487
line = self._current_line
@@ -90,9 +93,6 @@ def process_event(self, e):
9093
else:
9194
self.request_greenlet.switch(False)
9295
self.escape()
93-
elif e in ['<ESC>']:
94-
self.request_greenlet.switch(False)
95-
self.escape()
9696
else: # add normal character
9797
self.add_normal_character(e)
9898

bpython/curtsiesfrontend/interpreter.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import code
2+
import time
23
import traceback
34
import sys
45
from pygments.token import Generic, Token, Keyword, Name, Comment, String
56
from pygments.token import Error, Literal, Number, Operator, Punctuation
67
from pygments.token import Whitespace
78
from pygments.formatter import Formatter
89
from bpython.curtsiesfrontend.parse import parse
10+
from bpython.repl import RuntimeTimer
911
from codeop import CommandCompiler
1012
from pygments.lexers import get_lexer_by_name
1113

@@ -77,6 +79,7 @@ def __init__(self, locals=None):
7779
# typically changed after being instantiated
7880
self.write = lambda stuff: sys.stderr.write(stuff)
7981
self.outfile = self
82+
self.timer = RuntimeTimer()
8083

8184
def showsyntaxerror(self, filename=None):
8285
"""Display the syntax error that just occurred.
@@ -129,18 +132,18 @@ def showtraceback(self):
129132
tbtext = ''.join(l)
130133
lexer = get_lexer_by_name("pytb", stripall=True)
131134

132-
self.format(tbtext,lexer)
135+
self.format(tbtext, lexer)
133136

134137
def format(self, tbtext, lexer):
135138
traceback_informative_formatter = BPythonFormatter(default_colors)
136139
traceback_code_formatter = BPythonFormatter({Token: ('d')})
137-
tokens= list(lexer.get_tokens(tbtext))
140+
tokens = list(lexer.get_tokens(tbtext))
138141

139142
no_format_mode = False
140143
cur_line = []
141144
for token, text in tokens:
142145
if text.endswith('\n'):
143-
cur_line.append((token,text))
146+
cur_line.append((token, text))
144147
if no_format_mode:
145148
traceback_code_formatter.format(cur_line, self.outfile)
146149
no_format_mode = False
@@ -149,11 +152,17 @@ def format(self, tbtext, lexer):
149152
cur_line = []
150153
elif text == ' ' and cur_line == []:
151154
no_format_mode = True
152-
cur_line.append((token,text))
155+
cur_line.append((token, text))
153156
else:
154-
cur_line.append((token,text))
157+
cur_line.append((token, text))
155158
assert cur_line == [], cur_line
156159

160+
def runsource(self, source, filename="<input>", symbol="single"):
161+
with self.timer:
162+
return code.InteractiveInterpreter.runsource(
163+
self, source, filename=filename, symbol=symbol)
164+
165+
157166

158167
def code_finished_will_parse(s, compiler):
159168
"""Returns a tuple of whether the buffer could be complete and whether it will parse

bpython/curtsiesfrontend/repl.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def __init__(self,
219219
request_refresh=lambda: None,
220220
schedule_refresh=lambda when=0: None,
221221
request_reload=lambda desc: None,
222+
request_undo=lambda n=1: None,
222223
get_term_hw=lambda:(50, 10),
223224
get_cursor_vertical_diff=lambda: 0,
224225
banner=None,
@@ -282,6 +283,7 @@ def smarter_request_reload(files_modified=()):
282283
else:
283284
pass
284285
self.request_reload = smarter_request_reload
286+
self.request_undo = request_undo
285287
self.get_term_hw = get_term_hw
286288
self.get_cursor_vertical_diff = get_cursor_vertical_diff
287289

@@ -469,6 +471,9 @@ def process_control_event(self, e):
469471
except IOError as e:
470472
self.status_bar.message(_('Executing PYTHONSTARTUP failed: %s') % (str(e)))
471473

474+
elif isinstance(e, bpythonevents.UndoEvent):
475+
self.undo(n=e.n)
476+
472477
elif self.stdin.has_focus:
473478
return self.stdin.process_event(e)
474479

@@ -540,7 +545,7 @@ def process_key_event(self, e):
540545
elif e in ("<Shift-TAB>",):
541546
self.on_tab(back=True)
542547
elif e in key_dispatch[self.config.undo_key]: #ctrl-r for undo
543-
self.undo()
548+
self.prompt_undo()
544549
elif e in key_dispatch[self.config.save_key]: # ctrl-s for save
545550
greenlet.greenlet(self.write2file).switch()
546551
elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin
@@ -1295,6 +1300,19 @@ def take_back_buffer_line(self):
12951300
self.display_buffer.pop()
12961301
self.buffer.pop()
12971302

1303+
def prompt_undo(self):
1304+
if self.buffer:
1305+
return self.take_back_buffer_line()
1306+
1307+
self.reevaluate()
1308+
1309+
def prompt_for_undo():
1310+
n = BpythonRepl.prompt_undo(self)
1311+
if n > 0:
1312+
self.request_undo(n=n)
1313+
1314+
greenlet.greenlet(prompt_for_undo).switch()
1315+
12981316
def reevaluate(self, insert_into_history=False):
12991317
"""bpython.Repl.undo calls this"""
13001318
if self.watcher: self.watcher.reset()

bpython/repl.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import sys
3535
import tempfile
3636
import textwrap
37+
import time
3738
import traceback
3839
import unicodedata
3940
from itertools import takewhile
@@ -53,6 +54,26 @@
5354
import bpython.autocomplete as autocomplete
5455

5556

57+
class RuntimeTimer(object):
58+
def __init__(self):
59+
self.reset_timer()
60+
self.time = time.monotonic if hasattr(time, 'monotonic') else time.time
61+
62+
def __enter__(self):
63+
self.start = self.time()
64+
65+
def __exit__(self, ty, val, tb):
66+
self.last_command = self.time() - self.start
67+
self.running_time += self.last_command
68+
return False
69+
70+
def reset_timer(self):
71+
self.running_time = 0.0
72+
self.last_command = 0.0
73+
74+
def estimate(self):
75+
return self.running_time - self.last_command
76+
5677
class Interpreter(code.InteractiveInterpreter):
5778

5879
def __init__(self, locals=None, encoding=None):
@@ -68,16 +89,28 @@ def __init__(self, locals=None, encoding=None):
6889
self.syntaxerror_callback = None
6990
# Unfortunately code.InteractiveInterpreter is a classic class, so no super()
7091
code.InteractiveInterpreter.__init__(self, locals)
92+
self.timer = RuntimeTimer()
93+
94+
def reset_running_time(self):
95+
self.running_time = 0
96+
97+
if py3:
7198

72-
if not py3:
99+
def runsource(self, source, filename="<input>", symbol="single"):
100+
with self.timer:
101+
return code.InteractiveInterpreter.runsource(self, source,
102+
filename, symbol)
103+
104+
else:
73105

74106
def runsource(self, source, filename='<input>', symbol='single',
75107
encode=True):
76-
if encode:
77-
source = '# coding: %s\n%s' % (self.encoding,
78-
source.encode(self.encoding))
79-
return code.InteractiveInterpreter.runsource(self, source,
80-
filename, symbol)
108+
with self.timer:
109+
if encode:
110+
source = '# coding: %s\n%s' % (self.encoding,
111+
source.encode(self.encoding))
112+
return code.InteractiveInterpreter.runsource(self, source,
113+
filename, symbol)
81114

82115
def showsyntaxerror(self, filename=None):
83116
"""Override the regular handler, the code's copied and pasted from
@@ -792,25 +825,51 @@ def insert_into_history(self, s):
792825
except RuntimeError as e:
793826
self.interact.notify(str(e))
794827

828+
def prompt_undo(self):
829+
"""Returns how many lines to undo, 0 means don't undo"""
830+
if (self.config.single_undo_time < 0 or
831+
self.interp.timer.estimate() < self.config.single_undo_time):
832+
return 1
833+
est = self.interp.timer.estimate()
834+
n = self.interact.file_prompt(
835+
_("Undo how many lines? (Undo will take up to ~%.1f seconds) [1]")
836+
% (est,))
837+
try:
838+
if n == '':
839+
n = '1'
840+
n = int(n)
841+
except ValueError:
842+
self.interact.notify(_('Undo canceled'), .1)
843+
return 0
844+
else:
845+
if n == 0:
846+
self.interact.notify(_('Undo canceled'), .1)
847+
return 0
848+
if n == 1:
849+
self.interact.notify(_('Undoing 1 line... (est. %.1f seconds)')
850+
% (est,), .1)
851+
else:
852+
self.interact.notify(_('Undoing %d lines... (est. %.1f seconds)')
853+
% (n, est), .1)
854+
return n
855+
795856
def undo(self, n=1):
796-
"""Go back in the undo history n steps and call reeavluate()
857+
"""Go back in the undo history n steps and call reevaluate()
797858
Note that in the program this is called "Rewind" because I
798859
want it to be clear that this is by no means a true undo
799860
implementation, it is merely a convenience bonus."""
800861
if not self.history:
801862
return None
802863

864+
self.interp.timer.reset_timer()
865+
803866
if len(self.history) < n:
804867
n = len(self.history)
805868

806869
entries = list(self.rl_history.entries)
807870

808871
self.history = self.history[:-n]
809-
if (n == 1 and self.buffer and
810-
hasattr(self, 'take_back_buffer_line')):
811-
self.take_back_buffer_line()
812-
else:
813-
self.reevaluate()
872+
self.reevaluate()
814873

815874
self.rl_history.entries = entries
816875

bpython/sample-config

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
# bpython’s behalf. If unset, bpython uploads pastes to bpaste.net. (default: )
4949
#pastebin_helper = gist.py
5050

51+
# How long an undo must be expected to take before prompting for how
52+
# many lines should be undone. Set to -1 to never prompt, or 0 to
53+
# always prompt.
54+
# single_undo_time = 1.0
55+
5156
[keyboard]
5257

5358
# All key bindings are shown commented out with their default binding

doc/sphinx/source/configuration-options.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ following helper program can be used to create `gists
120120
121121
.. versionadded:: 0.12
122122

123+
124+
single_undo_time
125+
^^^^^^^^^^^^^^^^
126+
Time duration an undo must be predicted to take before prompting
127+
to undo multiple lines at once. Use -1 to never prompt, or 0 to always prompt.
128+
(default: 1.0)
129+
130+
.. versionadded:: 0.14
131+
123132
.. _configuration_color_scheme:
124133

125134
color_scheme

0 commit comments

Comments
 (0)
X Tutup