Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4969192

Browse files
bpo-36419: IDLE - Refactor autocompete and improve testing. (GH-15121)
(cherry picked from commit 1213123) Co-authored-by: Terry Jan Reedy <[email protected]>
1 parent a96f036 commit 4969192

File tree

4 files changed

+217
-177
lines changed

4 files changed

+217
-177
lines changed

Lib/idlelib/autocomplete.py

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,45 @@
88
import string
99
import sys
1010

11-
# These constants represent the two different types of completions.
12-
# They must be defined here so autocomple_w can import them.
13-
COMPLETE_ATTRIBUTES, COMPLETE_FILES = range(1, 2+1)
14-
11+
# Two types of completions; defined here for autocomplete_w import below.
12+
ATTRS, FILES = 0, 1
1513
from idlelib import autocomplete_w
1614
from idlelib.config import idleConf
1715
from idlelib.hyperparser import HyperParser
1816

17+
# Tuples passed to open_completions.
18+
# EvalFunc, Complete, WantWin, Mode
19+
FORCE = True, False, True, None # Control-Space.
20+
TAB = False, True, True, None # Tab.
21+
TRY_A = False, False, False, ATTRS # '.' for attributes.
22+
TRY_F = False, False, False, FILES # '/' in quotes for file name.
23+
1924
# This string includes all chars that may be in an identifier.
2025
# TODO Update this here and elsewhere.
2126
ID_CHARS = string.ascii_letters + string.digits + "_"
2227

23-
SEPS = os.sep
24-
if os.altsep: # e.g. '/' on Windows...
25-
SEPS += os.altsep
26-
28+
SEPS = f"{os.sep}{os.altsep if os.altsep else ''}"
29+
TRIGGERS = f".{SEPS}"
2730

2831
class AutoComplete:
2932

3033
def __init__(self, editwin=None):
3134
self.editwin = editwin
32-
if editwin is not None: # not in subprocess or test
35+
if editwin is not None: # not in subprocess or no-gui test
3336
self.text = editwin.text
34-
self.autocompletewindow = None
35-
# id of delayed call, and the index of the text insert when
36-
# the delayed call was issued. If _delayed_completion_id is
37-
# None, there is no delayed call.
38-
self._delayed_completion_id = None
39-
self._delayed_completion_index = None
37+
self.autocompletewindow = None
38+
# id of delayed call, and the index of the text insert when
39+
# the delayed call was issued. If _delayed_completion_id is
40+
# None, there is no delayed call.
41+
self._delayed_completion_id = None
42+
self._delayed_completion_index = None
4043

4144
@classmethod
4245
def reload(cls):
4346
cls.popupwait = idleConf.GetOption(
4447
"extensions", "AutoComplete", "popupwait", type="int", default=0)
4548

46-
def _make_autocomplete_window(self):
49+
def _make_autocomplete_window(self): # Makes mocking easier.
4750
return autocomplete_w.AutoCompleteWindow(self.text)
4851

4952
def _remove_autocomplete_window(self, event=None):
@@ -52,30 +55,12 @@ def _remove_autocomplete_window(self, event=None):
5255
self.autocompletewindow = None
5356

5457
def force_open_completions_event(self, event):
55-
"""Happens when the user really wants to open a completion list, even
56-
if a function call is needed.
57-
"""
58-
self.open_completions(True, False, True)
58+
"(^space) Open completion list, even if a function call is needed."
59+
self.open_completions(FORCE)
5960
return "break"
6061

61-
def try_open_completions_event(self, event):
62-
"""Happens when it would be nice to open a completion list, but not
63-
really necessary, for example after a dot, so function
64-
calls won't be made.
65-
"""
66-
lastchar = self.text.get("insert-1c")
67-
if lastchar == ".":
68-
self._open_completions_later(False, False, False,
69-
COMPLETE_ATTRIBUTES)
70-
elif lastchar in SEPS:
71-
self._open_completions_later(False, False, False,
72-
COMPLETE_FILES)
73-
7462
def autocomplete_event(self, event):
75-
"""Happens when the user wants to complete his word, and if necessary,
76-
open a completion list after that (if there is more than one
77-
completion)
78-
"""
63+
"(tab) Complete word or open list if multiple options."
7964
if hasattr(event, "mc_state") and event.mc_state or\
8065
not self.text.get("insert linestart", "insert").strip():
8166
# A modifier was pressed along with the tab or
@@ -85,34 +70,34 @@ def autocomplete_event(self, event):
8570
self.autocompletewindow.complete()
8671
return "break"
8772
else:
88-
opened = self.open_completions(False, True, True)
73+
opened = self.open_completions(TAB)
8974
return "break" if opened else None
9075

91-
def _open_completions_later(self, *args):
92-
self._delayed_completion_index = self.text.index("insert")
93-
if self._delayed_completion_id is not None:
94-
self.text.after_cancel(self._delayed_completion_id)
95-
self._delayed_completion_id = \
96-
self.text.after(self.popupwait, self._delayed_open_completions,
97-
*args)
98-
99-
def _delayed_open_completions(self, *args):
76+
def try_open_completions_event(self, event=None):
77+
"(./) Open completion list after pause with no movement."
78+
lastchar = self.text.get("insert-1c")
79+
if lastchar in TRIGGERS:
80+
args = TRY_A if lastchar == "." else TRY_F
81+
self._delayed_completion_index = self.text.index("insert")
82+
if self._delayed_completion_id is not None:
83+
self.text.after_cancel(self._delayed_completion_id)
84+
self._delayed_completion_id = self.text.after(
85+
self.popupwait, self._delayed_open_completions, args)
86+
87+
def _delayed_open_completions(self, args):
88+
"Call open_completions if index unchanged."
10089
self._delayed_completion_id = None
10190
if self.text.index("insert") == self._delayed_completion_index:
102-
self.open_completions(*args)
91+
self.open_completions(args)
10392

104-
def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
93+
def open_completions(self, args):
10594
"""Find the completions and create the AutoCompleteWindow.
10695
Return True if successful (no syntax error or so found).
10796
If complete is True, then if there's nothing to complete and no
10897
start of completion, won't open completions and return False.
10998
If mode is given, will open a completion list only in this mode.
110-
111-
Action Function Eval Complete WantWin Mode
112-
^space force_open_completions True, False, True no
113-
. or / try_open_completions False, False, False yes
114-
tab autocomplete False, True, True no
11599
"""
100+
evalfuncs, complete, wantwin, mode = args
116101
# Cancel another delayed call, if it exists.
117102
if self._delayed_completion_id is not None:
118103
self.text.after_cancel(self._delayed_completion_id)
@@ -121,14 +106,14 @@ def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
121106
hp = HyperParser(self.editwin, "insert")
122107
curline = self.text.get("insert linestart", "insert")
123108
i = j = len(curline)
124-
if hp.is_in_string() and (not mode or mode==COMPLETE_FILES):
109+
if hp.is_in_string() and (not mode or mode==FILES):
125110
# Find the beginning of the string.
126111
# fetch_completions will look at the file system to determine
127112
# whether the string value constitutes an actual file name
128113
# XXX could consider raw strings here and unescape the string
129114
# value if it's not raw.
130115
self._remove_autocomplete_window()
131-
mode = COMPLETE_FILES
116+
mode = FILES
132117
# Find last separator or string start
133118
while i and curline[i-1] not in "'\"" + SEPS:
134119
i -= 1
@@ -138,17 +123,17 @@ def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
138123
while i and curline[i-1] not in "'\"":
139124
i -= 1
140125
comp_what = curline[i:j]
141-
elif hp.is_in_code() and (not mode or mode==COMPLETE_ATTRIBUTES):
126+
elif hp.is_in_code() and (not mode or mode==ATTRS):
142127
self._remove_autocomplete_window()
143-
mode = COMPLETE_ATTRIBUTES
128+
mode = ATTRS
144129
while i and (curline[i-1] in ID_CHARS or ord(curline[i-1]) > 127):
145130
i -= 1
146131
comp_start = curline[i:j]
147-
if i and curline[i-1] == '.':
132+
if i and curline[i-1] == '.': # Need object with attributes.
148133
hp.set_index("insert-%dc" % (len(curline)-(i-1)))
149134
comp_what = hp.get_expression()
150-
if not comp_what or \
151-
(not evalfuncs and comp_what.find('(') != -1):
135+
if (not comp_what or
136+
(not evalfuncs and comp_what.find('(') != -1)):
152137
return None
153138
else:
154139
comp_what = ""
@@ -163,7 +148,7 @@ def open_completions(self, evalfuncs, complete, userWantsWin, mode=None):
163148
self.autocompletewindow = self._make_autocomplete_window()
164149
return not self.autocompletewindow.show_window(
165150
comp_lists, "insert-%dc" % len(comp_start),
166-
complete, mode, userWantsWin)
151+
complete, mode, wantwin)
167152

168153
def fetch_completions(self, what, mode):
169154
"""Return a pair of lists of completions for something. The first list
@@ -185,7 +170,7 @@ def fetch_completions(self, what, mode):
185170
return rpcclt.remotecall("exec", "get_the_completion_list",
186171
(what, mode), {})
187172
else:
188-
if mode == COMPLETE_ATTRIBUTES:
173+
if mode == ATTRS:
189174
if what == "":
190175
namespace = {**__main__.__builtins__.__dict__,
191176
**__main__.__dict__}
@@ -207,7 +192,7 @@ def fetch_completions(self, what, mode):
207192
except:
208193
return [], []
209194

210-
elif mode == COMPLETE_FILES:
195+
elif mode == FILES:
211196
if what == "":
212197
what = "."
213198
try:

Lib/idlelib/autocomplete_w.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from tkinter import *
77
from tkinter.ttk import Frame, Scrollbar
88

9-
from idlelib.autocomplete import COMPLETE_FILES, COMPLETE_ATTRIBUTES
9+
from idlelib.autocomplete import FILES, ATTRS
1010
from idlelib.multicall import MC_SHIFT
1111

1212
HIDE_VIRTUAL_EVENT_NAME = "<<autocompletewindow-hide>>"
@@ -39,8 +39,7 @@ def __init__(self, widget):
3939
self.completions = None
4040
# A list with more completions, or None
4141
self.morecompletions = None
42-
# The completion mode. Either autocomplete.COMPLETE_ATTRIBUTES or
43-
# autocomplete.COMPLETE_FILES
42+
# The completion mode, either autocomplete.ATTRS or .FILES.
4443
self.mode = None
4544
# The current completion start, on the text box (a string)
4645
self.start = None
@@ -73,8 +72,8 @@ def _change_start(self, newstart):
7372

7473
def _binary_search(self, s):
7574
"""Find the first index in self.completions where completions[i] is
76-
greater or equal to s, or the last index if there is no such
77-
one."""
75+
greater or equal to s, or the last index if there is no such.
76+
"""
7877
i = 0; j = len(self.completions)
7978
while j > i:
8079
m = (i + j) // 2
@@ -87,7 +86,8 @@ def _binary_search(self, s):
8786
def _complete_string(self, s):
8887
"""Assuming that s is the prefix of a string in self.completions,
8988
return the longest string which is a prefix of all the strings which
90-
s is a prefix of them. If s is not a prefix of a string, return s."""
89+
s is a prefix of them. If s is not a prefix of a string, return s.
90+
"""
9191
first = self._binary_search(s)
9292
if self.completions[first][:len(s)] != s:
9393
# There is not even one completion which s is a prefix of.
@@ -116,8 +116,10 @@ def _complete_string(self, s):
116116
return first_comp[:i]
117117

118118
def _selection_changed(self):
119-
"""Should be called when the selection of the Listbox has changed.
120-
Updates the Listbox display and calls _change_start."""
119+
"""Call when the selection of the Listbox has changed.
120+
121+
Updates the Listbox display and calls _change_start.
122+
"""
121123
cursel = int(self.listbox.curselection()[0])
122124

123125
self.listbox.see(cursel)
@@ -153,8 +155,10 @@ def _selection_changed(self):
153155

154156
def show_window(self, comp_lists, index, complete, mode, userWantsWin):
155157
"""Show the autocomplete list, bind events.
156-
If complete is True, complete the text, and if there is exactly one
157-
matching completion, don't open a list."""
158+
159+
If complete is True, complete the text, and if there is exactly
160+
one matching completion, don't open a list.
161+
"""
158162
# Handle the start we already have
159163
self.completions, self.morecompletions = comp_lists
160164
self.mode = mode
@@ -300,7 +304,7 @@ def keypress_event(self, event):
300304
if keysym != "Tab":
301305
self.lastkey_was_tab = False
302306
if (len(keysym) == 1 or keysym in ("underscore", "BackSpace")
303-
or (self.mode == COMPLETE_FILES and keysym in
307+
or (self.mode == FILES and keysym in
304308
("period", "minus"))) \
305309
and not (state & ~MC_SHIFT):
306310
# Normal editing of text
@@ -329,18 +333,18 @@ def keypress_event(self, event):
329333
self.hide_window()
330334
return 'break'
331335

332-
elif (self.mode == COMPLETE_ATTRIBUTES and keysym in
336+
elif (self.mode == ATTRS and keysym in
333337
("period", "space", "parenleft", "parenright", "bracketleft",
334338
"bracketright")) or \
335-
(self.mode == COMPLETE_FILES and keysym in
339+
(self.mode == FILES and keysym in
336340
("slash", "backslash", "quotedbl", "apostrophe")) \
337341
and not (state & ~MC_SHIFT):
338342
# If start is a prefix of the selection, but is not '' when
339343
# completing file names, put the whole
340344
# selected completion. Anyway, close the list.
341345
cursel = int(self.listbox.curselection()[0])
342346
if self.completions[cursel][:len(self.start)] == self.start \
343-
and (self.mode == COMPLETE_ATTRIBUTES or self.start):
347+
and (self.mode == ATTRS or self.start):
344348
self._change_start(self.completions[cursel])
345349
self.hide_window()
346350
return None

0 commit comments

Comments
 (0)