Skip to content

Commit 3ba6e16

Browse files
Brackets auto-close (bpython#934)
Auto-close for charcter pairs such as (), [], "", and ''. Not enabled by default, can be enabled in the bpython config file.
1 parent b4578ba commit 3ba6e16

File tree

6 files changed

+295
-7
lines changed

6 files changed

+295
-7
lines changed

bpython/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ class Config:
156156
"syntax": True,
157157
"tab_length": 4,
158158
"unicode_box": True,
159+
"brackets_completion": False,
159160
},
160161
"keyboard": {
161162
"backspace": "C-h",
@@ -362,6 +363,9 @@ def get_key_no_doublebind(command: str) -> str:
362363
if self.unicode_box and supports_box_chars()
363364
else ("|", "|", "-", "-", "+", "+", "+", "+")
364365
)
366+
self.brackets_completion = config.getboolean(
367+
"general", "brackets_completion"
368+
)
365369

366370

367371
def load_theme(

bpython/curtsiesfrontend/manual_readline.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
based on http://www.bigsmoke.us/readline/shortcuts"""
66

77
from ..lazyre import LazyReCompile
8-
98
import inspect
109

10+
from ..line import cursor_on_closing_char_pair
11+
1112
INDENT = 4
1213

1314
# TODO Allow user config of keybindings for these actions
@@ -244,6 +245,18 @@ def backspace(cursor_offset, line):
244245
cursor_offset - to_delete,
245246
line[: cursor_offset - to_delete] + line[cursor_offset:],
246247
)
248+
# removes opening bracket along with closing bracket
249+
# if there is nothing between them
250+
# TODO: could not get config value here, works even without -B option
251+
on_closing_char, pair_close = cursor_on_closing_char_pair(
252+
cursor_offset, line
253+
)
254+
if on_closing_char and pair_close:
255+
return (
256+
cursor_offset - 1,
257+
line[: cursor_offset - 1] + line[cursor_offset + 1 :],
258+
)
259+
247260
return (cursor_offset - 1, line[: cursor_offset - 1] + line[cursor_offset:])
248261

249262

bpython/curtsiesfrontend/repl.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
Interp,
4747
code_finished_will_parse,
4848
)
49-
from .manual_readline import edit_keys
49+
from .manual_readline import edit_keys, cursor_on_closing_char_pair
5050
from .parse import parse as bpythonparse, func_for_letter, color_for_letter
5151
from .preprocess import preprocess
5252
from .. import __version__
@@ -58,6 +58,7 @@
5858
SourceNotFound,
5959
)
6060
from ..translations import _
61+
from ..line import CHARACTER_PAIR_MAP
6162

6263
logger = logging.getLogger(__name__)
6364

@@ -800,9 +801,74 @@ def process_key_event(self, e: str) -> None:
800801
self.incr_search_mode = None
801802
elif e in ("<SPACE>",):
802803
self.add_normal_character(" ")
804+
elif e in CHARACTER_PAIR_MAP.keys():
805+
if e in ["'", '"']:
806+
if self.is_closing_quote(e):
807+
self.insert_char_pair_end(e)
808+
else:
809+
self.insert_char_pair_start(e)
810+
else:
811+
self.insert_char_pair_start(e)
812+
elif e in CHARACTER_PAIR_MAP.values():
813+
self.insert_char_pair_end(e)
803814
else:
804815
self.add_normal_character(e)
805816

817+
def is_closing_quote(self, e):
818+
char_count = self._current_line.count(e)
819+
if (
820+
char_count % 2 == 0
821+
and cursor_on_closing_char_pair(
822+
self._cursor_offset, self._current_line, e
823+
)[0]
824+
):
825+
return True
826+
return False
827+
828+
def insert_char_pair_start(self, e):
829+
"""Accepts character which is a part of CHARACTER_PAIR_MAP
830+
like brackets and quotes, and appends it to the line with
831+
an appropriate character pair ending. Closing character can only be inserted
832+
when the next character is either a closing character or a space
833+
834+
e.x. if you type "(" (lparen) , this will insert "()"
835+
into the line
836+
"""
837+
self.add_normal_character(e)
838+
if self.config.brackets_completion:
839+
allowed_chars = ["}", ")", "]", " "]
840+
start_of_line = len(self._current_line) == 1
841+
end_of_line = len(self._current_line) == self._cursor_offset
842+
can_lookup_next = len(self._current_line) > self._cursor_offset
843+
next_char = (
844+
None
845+
if not can_lookup_next
846+
else self._current_line[self._cursor_offset]
847+
)
848+
next_char_allowed = next_char in allowed_chars
849+
if start_of_line or end_of_line or next_char_allowed:
850+
closing_char = CHARACTER_PAIR_MAP[e]
851+
self.add_normal_character(closing_char, narrow_search=False)
852+
self._cursor_offset -= 1
853+
854+
def insert_char_pair_end(self, e):
855+
"""Accepts character which is a part of CHARACTER_PAIR_MAP
856+
like brackets and quotes, and checks whether it should be
857+
inserted to the line or overwritten
858+
859+
e.x. if you type ")" (rparen) , and your cursor is directly
860+
above another ")" (rparen) in the cmd, this will just skip
861+
it and move the cursor.
862+
If there is no same character underneath the cursor, the
863+
character will be printed/appended to the line
864+
"""
865+
if self.config.brackets_completion:
866+
if self.cursor_offset < len(self._current_line):
867+
if self._current_line[self.cursor_offset] == e:
868+
self.cursor_offset += 1
869+
return
870+
self.add_normal_character(e)
871+
806872
def get_last_word(self):
807873

808874
previous_word = _last_word(self.rl_history.entry)
@@ -903,7 +969,15 @@ def only_whitespace_left_of_cursor():
903969
for unused in range(to_add):
904970
self.add_normal_character(" ")
905971
return
906-
972+
# if cursor on closing character from pair,
973+
# moves cursor behind it on tab
974+
# ? should we leave it here as default?
975+
if self.config.brackets_completion:
976+
on_closing_char, _ = cursor_on_closing_char_pair(
977+
self._cursor_offset, self._current_line
978+
)
979+
if on_closing_char:
980+
self._cursor_offset += 1
907981
# run complete() if we don't already have matches
908982
if len(self.matches_iter.matches) == 0:
909983
self.list_win_visible = self.complete(tab=True)
@@ -915,7 +989,6 @@ def only_whitespace_left_of_cursor():
915989
# using _current_line so we don't trigger a completion reset
916990
if not self.matches_iter.matches:
917991
self.list_win_visible = self.complete()
918-
919992
elif self.matches_iter.matches:
920993
self.current_match = (
921994
back and self.matches_iter.previous() or next(self.matches_iter)
@@ -924,6 +997,24 @@ def only_whitespace_left_of_cursor():
924997
self._cursor_offset, self._current_line = cursor_and_line
925998
# using _current_line so we don't trigger a completion reset
926999
self.list_win_visible = True
1000+
if self.config.brackets_completion:
1001+
# appends closing char pair if completion is a callable
1002+
if self.is_completion_callable(self._current_line):
1003+
self._current_line = self.append_closing_character(
1004+
self._current_line
1005+
)
1006+
1007+
def is_completion_callable(self, completion):
1008+
"""Checks whether given completion is callable (e.x. function)"""
1009+
completion_end = completion[-1]
1010+
return completion_end in CHARACTER_PAIR_MAP
1011+
1012+
def append_closing_character(self, completion):
1013+
"""Appends closing character/bracket to the completion"""
1014+
completion_end = completion[-1]
1015+
if completion_end in CHARACTER_PAIR_MAP:
1016+
completion = f"{completion}{CHARACTER_PAIR_MAP[completion_end]}"
1017+
return completion
9271018

9281019
def on_control_d(self):
9291020
if self.current_line == "":
@@ -1071,7 +1162,7 @@ def toggle_file_watch(self):
10711162
)
10721163

10731164
# Handler Helpers
1074-
def add_normal_character(self, char):
1165+
def add_normal_character(self, char, narrow_search=True):
10751166
if len(char) > 1 or is_nop(char):
10761167
return
10771168
if self.incr_search_mode:
@@ -1087,12 +1178,18 @@ def add_normal_character(self, char):
10871178
reset_rl_history=False,
10881179
clear_special_mode=False,
10891180
)
1090-
self.cursor_offset += 1
1181+
if narrow_search:
1182+
self.cursor_offset += 1
1183+
else:
1184+
self._cursor_offset += 1
10911185
if self.config.cli_trim_prompts and self.current_line.startswith(
10921186
self.ps1
10931187
):
10941188
self.current_line = self.current_line[4:]
1095-
self.cursor_offset = max(0, self.cursor_offset - 4)
1189+
if narrow_search:
1190+
self.cursor_offset = max(0, self.cursor_offset - 4)
1191+
else:
1192+
self._cursor_offset += max(0, self.cursor_offset - 4)
10961193

10971194
def add_to_incremental_search(self, char=None, backspace=False):
10981195
"""Modify the current search term while in incremental search.

bpython/line.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class LinePart(NamedTuple):
1919

2020

2121
_current_word_re = LazyReCompile(r"(?<![)\]\w_.])" r"([\w_][\w0-9._]*[(]?)")
22+
CHARACTER_PAIR_MAP = {"(": ")", "{": "}", "[": "]", "'": "'", '"': '"'}
2223

2324

2425
def current_word(cursor_offset: int, line: str) -> Optional[LinePart]:
@@ -287,3 +288,25 @@ def current_expression_attribute(
287288
if m.start(1) <= cursor_offset <= m.end(1):
288289
return LinePart(m.start(1), m.end(1), m.group(1))
289290
return None
291+
292+
293+
def cursor_on_closing_char_pair(cursor_offset, line, ch=None):
294+
"""Checks if cursor sits on closing character of a pair
295+
and whether its pair character is directly behind it
296+
"""
297+
on_closing_char, pair_close = False, False
298+
if line is None:
299+
return on_closing_char, pair_close
300+
if cursor_offset < len(line):
301+
cur_char = line[cursor_offset]
302+
if cur_char in CHARACTER_PAIR_MAP.values():
303+
on_closing_char = True if not ch else cur_char == ch
304+
if cursor_offset > 0:
305+
prev_char = line[cursor_offset - 1]
306+
if (
307+
on_closing_char
308+
and prev_char in CHARACTER_PAIR_MAP
309+
and CHARACTER_PAIR_MAP[prev_char] == cur_char
310+
):
311+
pair_close = True if not ch else prev_char == ch
312+
return on_closing_char, pair_close

bpython/sample-config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161

6262
# Enable autoreload feature by default (default: False).
6363
# default_autoreload = False
64+
# Enable autocompletion of brackets and quotes (default: False)
65+
# brackets_completion = False
6466

6567
[keyboard]
6668

0 commit comments

Comments
 (0)