Skip to content

Commit 6b8a076

Browse files
Merge branch 'pastebuffer-keys'
Adds readline keys. ctrl-y and meta-y both yank for now
2 parents efb3174 + c92b043 commit 6b8a076

File tree

4 files changed

+272
-72
lines changed

4 files changed

+272
-72
lines changed

bpython/curtsiesfrontend/interaction.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from bpython.repl import Interaction as BpythonInteraction
66

7-
from bpython.curtsiesfrontend.manual_readline import char_sequences as rl_char_sequences
7+
from bpython.curtsiesfrontend.manual_readline import edit_keys
88

99
class StatusBar(BpythonInteraction):
1010
"""StatusBar and Interaction for Repl
@@ -68,8 +68,8 @@ def process_event(self, e):
6868
elif isinstance(e, events.PasteEvent):
6969
for ee in e.events:
7070
self.add_normal_character(ee if len(ee) == 1 else ee[-1]) #strip control seq
71-
elif e in rl_char_sequences:
72-
self.cursor_offset_in_line, self._current_line = rl_char_sequences[e](self.cursor_offset_in_line, self._current_line)
71+
elif e in edit_keys:
72+
self.cursor_offset_in_line, self._current_line = edit_keys[e](self.cursor_offset_in_line, self._current_line)
7373
elif e == "<Ctrl-c>":
7474
raise KeyboardInterrupt()
7575
elif e == "<Ctrl-d>":
Lines changed: 187 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,196 @@
1-
"""implementations of simple readline control sequences
1+
"""implementations of simple readline edit operations
22
33
just the ones that fit the model of transforming the current line
44
and the cursor location
5-
in the order of description at http://www.bigsmoke.us/readline/shortcuts"""
5+
based on http://www.bigsmoke.us/readline/shortcuts"""
66

77
import re
8-
char_sequences = {}
8+
import inspect
99

1010
INDENT = 4
1111

1212
#TODO Allow user config of keybindings for these actions
1313

14-
def on(seq):
15-
def add_to_char_sequences(func):
16-
char_sequences[seq] = func
17-
return func
18-
return add_to_char_sequences
14+
class AbstractEdits(object):
1915

20-
@on('<Ctrl-b>')
21-
@on('<LEFT>')
16+
default_kwargs = {
17+
'line': 'hello world',
18+
'cursor_offset': 5,
19+
'cut_buffer': 'there',
20+
}
21+
22+
def __contains__(self, key):
23+
try:
24+
self[key]
25+
except KeyError:
26+
return False
27+
else:
28+
return True
29+
30+
def add(self, key, func, overwrite=False):
31+
if key in self:
32+
if overwrite:
33+
del self[key]
34+
else:
35+
raise ValueError('key %r already has a mapping' % (key,))
36+
params = inspect.getargspec(func)[0]
37+
args = dict((k, v) for k, v in self.default_kwargs.items() if k in params)
38+
r = func(**args)
39+
if len(r) == 2:
40+
if hasattr(func, 'kills'):
41+
raise ValueError('function %r returns two values, but has a kills attribute' % (func,))
42+
self.simple_edits[key] = func
43+
elif len(r) == 3:
44+
if not hasattr(func, 'kills'):
45+
raise ValueError('function %r returns three values, but has no kills attribute' % (func,))
46+
self.cut_buffer_edits[key] = func
47+
else:
48+
raise ValueError('return type of function %r not recognized' % (func,))
49+
50+
def add_config_attr(self, config_attr, func):
51+
if config_attr in self.awaiting_config:
52+
raise ValueError('config attrribute %r already has a mapping' % (config_attr,))
53+
self.awaiting_config[config_attr] = func
54+
55+
def call(self, key, **kwargs):
56+
func = self[key]
57+
params = inspect.getargspec(func)[0]
58+
args = dict((k, v) for k, v in kwargs.items() if k in params)
59+
return func(**args)
60+
61+
def call_without_cut(self, key, **kwargs):
62+
"""Looks up the function and calls it, returning only line and cursor offset"""
63+
r = self.call_for_two(key, **kwargs)
64+
return r[:2]
65+
66+
def __getitem__(self, key):
67+
if key in self.simple_edits: return self.simple_edits[key]
68+
if key in self.cut_buffer_edits: return self.cut_buffer_edits[key]
69+
raise KeyError("key %r not mapped" % (key,))
70+
71+
def __delitem__(self, key):
72+
if key in self.simple_edits: del self.simple_edits[key]
73+
elif key in self.cut_buffer_edits: del self.cut_buffer_edits[key]
74+
else: raise KeyError("key %r not mapped" % (key,))
75+
76+
class UnconfiguredEdits(AbstractEdits):
77+
"""Maps key to edit functions, and bins them by what parameters they take.
78+
79+
Only functions with specific signatures can be added:
80+
* func(**kwargs) -> cursor_offset, line
81+
* func(**kwargs) -> cursor_offset, line, cut_buffer
82+
where kwargs are in among the keys of Edits.default_kwargs
83+
These functions will be run to determine their return type, so no side effects!
84+
85+
More concrete Edits instances can be created by applying a config with
86+
Edits.mapping_with_config() - this creates a new Edits instance
87+
that uses a config file to assign config_attr bindings.
88+
89+
Keys can't be added twice, config attributes can't be added twice.
90+
"""
91+
92+
def __init__(self):
93+
self.simple_edits = {}
94+
self.cut_buffer_edits = {}
95+
self.awaiting_config = {}
96+
97+
def mapping_with_config(self, config, key_dispatch):
98+
"""Creates a new mapping object by applying a config object"""
99+
return ConfiguredEdits(self.simple_edits, self.cut_buffer_edits,
100+
self.awaiting_config, config, key_dispatch)
101+
102+
def on(self, key=None, config=None):
103+
if (key is None and config is None or
104+
key is not None and config is not None):
105+
raise ValueError("Must use exactly one of key, config")
106+
if key is not None:
107+
def add_to_keybinds(func):
108+
self.add(key, func)
109+
return func
110+
return add_to_keybinds
111+
else:
112+
def add_to_config(func):
113+
self.add_config_attr(config, func)
114+
return func
115+
return add_to_config
116+
117+
class ConfiguredEdits(AbstractEdits):
118+
def __init__(self, simple_edits, cut_buffer_edits, awaiting_config, config, key_dispatch):
119+
self.simple_edits = dict(simple_edits)
120+
self.cut_buffer_edits = dict(cut_buffer_edits)
121+
for attr, func in awaiting_config.items():
122+
for key in key_dispatch[getattr(config, attr)]:
123+
super(ConfiguredEdits, self).add(key, func, overwrite=True)
124+
125+
def add_config_attr(self, config_attr, func):
126+
raise NotImplementedError("Config already set on this mapping")
127+
128+
def add(self, key, func):
129+
raise NotImplementedError("Config already set on this mapping")
130+
131+
edit_keys = UnconfiguredEdits()
132+
133+
# Because the edits.on decorator runs the functions, functions which depend
134+
# on other functions must be declared after their dependencies
135+
136+
def kills_behind(func):
137+
func.kills = 'behind'
138+
return func
139+
140+
def kills_ahead(func):
141+
func.kills = 'ahead'
142+
return func
143+
144+
@edit_keys.on('<Ctrl-b>')
145+
@edit_keys.on('<LEFT>')
22146
def left_arrow(cursor_offset, line):
23147
return max(0, cursor_offset - 1), line
24148

25-
@on('<Ctrl-f>')
26-
@on('<RIGHT>')
149+
@edit_keys.on('<Ctrl-f>')
150+
@edit_keys.on('<RIGHT>')
27151
def right_arrow(cursor_offset, line):
28152
return min(len(line), cursor_offset + 1), line
29153

30-
@on('<Ctrl-a>')
31-
@on('<HOME>')
154+
@edit_keys.on('<Ctrl-a>')
155+
@edit_keys.on('<HOME>')
32156
def beginning_of_line(cursor_offset, line):
33157
return 0, line
34158

35-
@on('<Ctrl-e>')
36-
@on('<END>')
159+
@edit_keys.on('<Ctrl-e>')
160+
@edit_keys.on('<END>')
37161
def end_of_line(cursor_offset, line):
38162
return len(line), line
39163

40-
@on('<Esc+f>')
41-
@on('<Ctrl-RIGHT>')
42-
@on('<Esc+RIGHT>')
164+
@edit_keys.on('<Esc+f>')
165+
@edit_keys.on('<Ctrl-RIGHT>')
166+
@edit_keys.on('<Esc+RIGHT>')
43167
def forward_word(cursor_offset, line):
44168
patt = r"\S\s"
45169
match = re.search(patt, line[cursor_offset:]+' ')
46170
delta = match.end() - 1 if match else 0
47171
return (cursor_offset + delta, line)
48172

49-
@on('<Esc+b>')
50-
@on('<Ctrl-LEFT>')
51-
@on('<Esc+LEFT>')
52-
def back_word(cursor_offset, line):
53-
return (last_word_pos(line[:cursor_offset]), line)
54-
55173
def last_word_pos(string):
56174
"""returns the start index of the last word of given string"""
57175
patt = r'\S\s'
58176
match = re.search(patt, string[::-1])
59177
index = match and len(string) - match.end() + 1
60178
return index or 0
61179

62-
@on('<PADDELETE>')
180+
@edit_keys.on('<Esc+b>')
181+
@edit_keys.on('<Ctrl-LEFT>')
182+
@edit_keys.on('<Esc+LEFT>')
183+
def back_word(cursor_offset, line):
184+
return (last_word_pos(line[:cursor_offset]), line)
185+
186+
@edit_keys.on('<PADDELETE>')
63187
def delete(cursor_offset, line):
64188
return (cursor_offset,
65189
line[:cursor_offset] + line[cursor_offset+1:])
66190

67-
@on('<Ctrl-h>')
68-
@on('<BACKSPACE>')
191+
@edit_keys.on('<Ctrl-h>')
192+
@edit_keys.on('<BACKSPACE>')
193+
@edit_keys.on(config='delete_key')
69194
def backspace(cursor_offset, line):
70195
if cursor_offset == 0:
71196
return cursor_offset, line
@@ -76,72 +201,77 @@ def backspace(cursor_offset, line):
76201
return (cursor_offset - 1,
77202
line[:cursor_offset - 1] + line[cursor_offset:])
78203

79-
@on('<Ctrl-u>')
204+
@edit_keys.on('<Ctrl-u>')
205+
@edit_keys.on(config='clear_line_key')
80206
def delete_from_cursor_back(cursor_offset, line):
81207
return 0, line[cursor_offset:]
82208

83-
@on('<Ctrl-k>')
84-
def delete_from_cursor_forward(cursor_offset, line):
85-
return cursor_offset, line[:cursor_offset]
86-
87-
@on('<Esc+d>') # option-d
209+
@edit_keys.on('<Esc+d>') # option-d
210+
@kills_ahead
88211
def delete_rest_of_word(cursor_offset, line):
89212
m = re.search(r'\w\b', line[cursor_offset:])
90213
if not m:
91-
return cursor_offset, line
92-
return cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:]
214+
return cursor_offset, line, ''
215+
return (cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:],
216+
line[cursor_offset:m.start()+cursor_offset+1])
93217

94-
@on('<Ctrl-w>')
218+
@edit_keys.on('<Ctrl-w>')
219+
@edit_keys.on(config='clear_word_key')
220+
@kills_behind
95221
def delete_word_to_cursor(cursor_offset, line):
96222
matches = list(re.finditer(r'\s\S', line[:cursor_offset]))
97223
start = matches[-1].start()+1 if matches else 0
98-
return start, line[:start] + line[cursor_offset:]
224+
return start, line[:start] + line[cursor_offset:], line[start:cursor_offset]
99225

100-
@on('<Esc+y>')
101-
def yank_prev_prev_killed_text(cursor_offset, line):
102-
return cursor_offset, line #TODO Not implemented
226+
@edit_keys.on('<Esc+y>')
227+
def yank_prev_prev_killed_text(cursor_offset, line, cut_buffer): #TODO not implemented - just prev
228+
return cursor_offset+len(cut_buffer), line[:cursor_offset] + cut_buffer + line[cursor_offset:]
103229

104-
@on('<Ctrl-t>')
230+
@edit_keys.on(config='yank_from_buffer_key')
231+
def yank_prev_killed_text(cursor_offset, line, cut_buffer):
232+
return cursor_offset+len(cut_buffer), line[:cursor_offset] + cut_buffer + line[cursor_offset:]
233+
234+
@edit_keys.on('<Ctrl-t>')
105235
def transpose_character_before_cursor(cursor_offset, line):
106236
return (min(len(line), cursor_offset + 1),
107237
line[:cursor_offset-1] +
108238
(line[cursor_offset] if len(line) > cursor_offset else '') +
109239
line[cursor_offset - 1] +
110240
line[cursor_offset+1:])
111241

112-
@on('<Esc+t>')
242+
@edit_keys.on('<Esc+t>')
113243
def transpose_word_before_cursor(cursor_offset, line):
114244
return cursor_offset, line #TODO Not implemented
115245

116246
# bonus functions (not part of readline)
117247

118-
@on('<Esc+r>')
248+
@edit_keys.on('<Esc+r>')
119249
def delete_line(cursor_offset, line):
120250
return 0, ""
121251

122-
@on('<Esc+u>')
252+
@edit_keys.on('<Esc+u>')
123253
def uppercase_next_word(cursor_offset, line):
124254
return cursor_offset, line #TODO Not implemented
125255

126-
@on('<Esc+c>')
256+
@edit_keys.on('<Ctrl-k>')
257+
@kills_ahead
258+
def delete_from_cursor_forward(cursor_offset, line):
259+
return cursor_offset, line[:cursor_offset], line[cursor_offset:]
260+
261+
@edit_keys.on('<Esc+c>')
127262
def titlecase_next_word(cursor_offset, line):
128263
return cursor_offset, line #TODO Not implemented
129264

130-
@on('<Esc+BACKSPACE>')
131-
@on('<Meta-BACKSPACE>')
265+
@edit_keys.on('<Esc+BACKSPACE>')
266+
@edit_keys.on('<Meta-BACKSPACE>')
267+
@kills_behind
132268
def delete_word_from_cursor_back(cursor_offset, line):
133269
"""Whatever my option-delete does in bash on my mac"""
134270
if not line:
135271
return cursor_offset, line
136272
starts = [m.start() for m in list(re.finditer(r'\b\w', line)) if m.start() < cursor_offset]
137273
if starts:
138-
return starts[-1], line[:starts[-1]] + line[cursor_offset:]
139-
return cursor_offset, line
140-
141-
def get_updated_char_sequences(key_dispatch, config):
142-
updated_char_sequences = dict(char_sequences)
143-
updated_char_sequences[key_dispatch[config.delete_key]] = backspace
144-
updated_char_sequences[key_dispatch[config.clear_word_key]] = delete_word_to_cursor
145-
updated_char_sequences[key_dispatch[config.clear_line_key]] = delete_from_cursor_back
146-
return updated_char_sequences
274+
return starts[-1], line[:starts[-1]] + line[cursor_offset:], line[starts[-1]:cursor_offset]
275+
return cursor_offset, line, ''
276+
147277

0 commit comments

Comments
 (0)