Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/IPython/terminal/shortcuts/auto_suggest.py: 24%

195 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:07 +0000

1import re 

2import tokenize 

3from io import StringIO 

4from typing import Callable, List, Optional, Union, Generator, Tuple 

5 

6from prompt_toolkit.buffer import Buffer 

7from prompt_toolkit.key_binding import KeyPressEvent 

8from prompt_toolkit.key_binding.bindings import named_commands as nc 

9from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion 

10from prompt_toolkit.document import Document 

11from prompt_toolkit.history import History 

12from prompt_toolkit.shortcuts import PromptSession 

13from prompt_toolkit.layout.processors import ( 

14 Processor, 

15 Transformation, 

16 TransformationInput, 

17) 

18 

19from IPython.core.getipython import get_ipython 

20from IPython.utils.tokenutil import generate_tokens 

21 

22 

23def _get_query(document: Document): 

24 return document.lines[document.cursor_position_row] 

25 

26 

27class AppendAutoSuggestionInAnyLine(Processor): 

28 """ 

29 Append the auto suggestion to lines other than the last (appending to the 

30 last line is natively supported by the prompt toolkit). 

31 """ 

32 

33 def __init__(self, style: str = "class:auto-suggestion") -> None: 

34 self.style = style 

35 

36 def apply_transformation(self, ti: TransformationInput) -> Transformation: 

37 is_last_line = ti.lineno == ti.document.line_count - 1 

38 is_active_line = ti.lineno == ti.document.cursor_position_row 

39 

40 if not is_last_line and is_active_line: 

41 buffer = ti.buffer_control.buffer 

42 

43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line: 

44 suggestion = buffer.suggestion.text 

45 else: 

46 suggestion = "" 

47 

48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) 

49 else: 

50 return Transformation(fragments=ti.fragments) 

51 

52 

53class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): 

54 """ 

55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous 

56 suggestion from history. To do so it remembers the current position, but it 

57 state need to carefully be cleared on the right events. 

58 """ 

59 

60 def __init__( 

61 self, 

62 ): 

63 self.skip_lines = 0 

64 self._connected_apps = [] 

65 

66 def reset_history_position(self, _: Buffer): 

67 self.skip_lines = 0 

68 

69 def disconnect(self): 

70 for pt_app in self._connected_apps: 

71 text_insert_event = pt_app.default_buffer.on_text_insert 

72 text_insert_event.remove_handler(self.reset_history_position) 

73 

74 def connect(self, pt_app: PromptSession): 

75 self._connected_apps.append(pt_app) 

76 # note: `on_text_changed` could be used for a bit different behaviour 

77 # on character deletion (i.e. reseting history position on backspace) 

78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position) 

79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss) 

80 

81 def get_suggestion( 

82 self, buffer: Buffer, document: Document 

83 ) -> Optional[Suggestion]: 

84 text = _get_query(document) 

85 

86 if text.strip(): 

87 for suggestion, _ in self._find_next_match( 

88 text, self.skip_lines, buffer.history 

89 ): 

90 return Suggestion(suggestion) 

91 

92 return None 

93 

94 def _dismiss(self, buffer, *args, **kwargs): 

95 buffer.suggestion = None 

96 

97 def _find_match( 

98 self, text: str, skip_lines: float, history: History, previous: bool 

99 ) -> Generator[Tuple[str, float], None, None]: 

100 """ 

101 text : str 

102 Text content to find a match for, the user cursor is most of the 

103 time at the end of this text. 

104 skip_lines : float 

105 number of items to skip in the search, this is used to indicate how 

106 far in the list the user has navigated by pressing up or down. 

107 The float type is used as the base value is +inf 

108 history : History 

109 prompt_toolkit History instance to fetch previous entries from. 

110 previous : bool 

111 Direction of the search, whether we are looking previous match 

112 (True), or next match (False). 

113 

114 Yields 

115 ------ 

116 Tuple with: 

117 str: 

118 current suggestion. 

119 float: 

120 will actually yield only ints, which is passed back via skip_lines, 

121 which may be a +inf (float) 

122 

123 

124 """ 

125 line_number = -1 

126 for string in reversed(list(history.get_strings())): 

127 for line in reversed(string.splitlines()): 

128 line_number += 1 

129 if not previous and line_number < skip_lines: 

130 continue 

131 # do not return empty suggestions as these 

132 # close the auto-suggestion overlay (and are useless) 

133 if line.startswith(text) and len(line) > len(text): 

134 yield line[len(text) :], line_number 

135 if previous and line_number >= skip_lines: 

136 return 

137 

138 def _find_next_match( 

139 self, text: str, skip_lines: float, history: History 

140 ) -> Generator[Tuple[str, float], None, None]: 

141 return self._find_match(text, skip_lines, history, previous=False) 

142 

143 def _find_previous_match(self, text: str, skip_lines: float, history: History): 

144 return reversed( 

145 list(self._find_match(text, skip_lines, history, previous=True)) 

146 ) 

147 

148 def up(self, query: str, other_than: str, history: History) -> None: 

149 for suggestion, line_number in self._find_next_match( 

150 query, self.skip_lines, history 

151 ): 

152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very' 

153 # we want to switch from 'very.b' to 'very.a' because a) if the 

154 # suggestion equals current text, prompt-toolkit aborts suggesting 

155 # b) user likely would not be interested in 'very' anyways (they 

156 # already typed it). 

157 if query + suggestion != other_than: 

158 self.skip_lines = line_number 

159 break 

160 else: 

161 # no matches found, cycle back to beginning 

162 self.skip_lines = 0 

163 

164 def down(self, query: str, other_than: str, history: History) -> None: 

165 for suggestion, line_number in self._find_previous_match( 

166 query, self.skip_lines, history 

167 ): 

168 if query + suggestion != other_than: 

169 self.skip_lines = line_number 

170 break 

171 else: 

172 # no matches found, cycle to end 

173 for suggestion, line_number in self._find_previous_match( 

174 query, float("Inf"), history 

175 ): 

176 if query + suggestion != other_than: 

177 self.skip_lines = line_number 

178 break 

179 

180 

181# Needed for to accept autosuggestions in vi insert mode 

182def accept_in_vi_insert_mode(event: KeyPressEvent): 

183 """Apply autosuggestion if at end of line.""" 

184 buffer = event.current_buffer 

185 d = buffer.document 

186 after_cursor = d.text[d.cursor_position :] 

187 lines = after_cursor.split("\n") 

188 end_of_current_line = lines[0].strip() 

189 suggestion = buffer.suggestion 

190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): 

191 buffer.insert_text(suggestion.text) 

192 else: 

193 nc.end_of_line(event) 

194 

195 

196def accept(event: KeyPressEvent): 

197 """Accept autosuggestion""" 

198 buffer = event.current_buffer 

199 suggestion = buffer.suggestion 

200 if suggestion: 

201 buffer.insert_text(suggestion.text) 

202 else: 

203 nc.forward_char(event) 

204 

205 

206def discard(event: KeyPressEvent): 

207 """Discard autosuggestion""" 

208 buffer = event.current_buffer 

209 buffer.suggestion = None 

210 

211 

212def accept_word(event: KeyPressEvent): 

213 """Fill partial autosuggestion by word""" 

214 buffer = event.current_buffer 

215 suggestion = buffer.suggestion 

216 if suggestion: 

217 t = re.split(r"(\S+\s+)", suggestion.text) 

218 buffer.insert_text(next((x for x in t if x), "")) 

219 else: 

220 nc.forward_word(event) 

221 

222 

223def accept_character(event: KeyPressEvent): 

224 """Fill partial autosuggestion by character""" 

225 b = event.current_buffer 

226 suggestion = b.suggestion 

227 if suggestion and suggestion.text: 

228 b.insert_text(suggestion.text[0]) 

229 

230 

231def accept_and_keep_cursor(event: KeyPressEvent): 

232 """Accept autosuggestion and keep cursor in place""" 

233 buffer = event.current_buffer 

234 old_position = buffer.cursor_position 

235 suggestion = buffer.suggestion 

236 if suggestion: 

237 buffer.insert_text(suggestion.text) 

238 buffer.cursor_position = old_position 

239 

240 

241def accept_and_move_cursor_left(event: KeyPressEvent): 

242 """Accept autosuggestion and move cursor left in place""" 

243 accept_and_keep_cursor(event) 

244 nc.backward_char(event) 

245 

246 

247def _update_hint(buffer: Buffer): 

248 if buffer.auto_suggest: 

249 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) 

250 buffer.suggestion = suggestion 

251 

252 

253def backspace_and_resume_hint(event: KeyPressEvent): 

254 """Resume autosuggestions after deleting last character""" 

255 current_buffer = event.current_buffer 

256 

257 def resume_hinting(buffer: Buffer): 

258 _update_hint(buffer) 

259 current_buffer.on_text_changed.remove_handler(resume_hinting) 

260 

261 current_buffer.on_text_changed.add_handler(resume_hinting) 

262 nc.backward_delete_char(event) 

263 

264 

265def up_and_update_hint(event: KeyPressEvent): 

266 """Go up and update hint""" 

267 current_buffer = event.current_buffer 

268 

269 current_buffer.auto_up(count=event.arg) 

270 _update_hint(current_buffer) 

271 

272 

273def down_and_update_hint(event: KeyPressEvent): 

274 """Go down and update hint""" 

275 current_buffer = event.current_buffer 

276 

277 current_buffer.auto_down(count=event.arg) 

278 _update_hint(current_buffer) 

279 

280 

281def accept_token(event: KeyPressEvent): 

282 """Fill partial autosuggestion by token""" 

283 b = event.current_buffer 

284 suggestion = b.suggestion 

285 

286 if suggestion: 

287 prefix = _get_query(b.document) 

288 text = prefix + suggestion.text 

289 

290 tokens: List[Optional[str]] = [None, None, None] 

291 substrings = [""] 

292 i = 0 

293 

294 for token in generate_tokens(StringIO(text).readline): 

295 if token.type == tokenize.NEWLINE: 

296 index = len(text) 

297 else: 

298 index = text.index(token[1], len(substrings[-1])) 

299 substrings.append(text[:index]) 

300 tokenized_so_far = substrings[-1] 

301 if tokenized_so_far.startswith(prefix): 

302 if i == 0 and len(tokenized_so_far) > len(prefix): 

303 tokens[0] = tokenized_so_far[len(prefix) :] 

304 substrings.append(tokenized_so_far) 

305 i += 1 

306 tokens[i] = token[1] 

307 if i == 2: 

308 break 

309 i += 1 

310 

311 if tokens[0]: 

312 to_insert: str 

313 insert_text = substrings[-2] 

314 if tokens[1] and len(tokens[1]) == 1: 

315 insert_text = substrings[-1] 

316 to_insert = insert_text[len(prefix) :] 

317 b.insert_text(to_insert) 

318 return 

319 

320 nc.forward_word(event) 

321 

322 

323Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] 

324 

325 

326def _swap_autosuggestion( 

327 buffer: Buffer, 

328 provider: NavigableAutoSuggestFromHistory, 

329 direction_method: Callable, 

330): 

331 """ 

332 We skip most recent history entry (in either direction) if it equals the 

333 current autosuggestion because if user cycles when auto-suggestion is shown 

334 they most likely want something else than what was suggested (otherwise 

335 they would have accepted the suggestion). 

336 """ 

337 suggestion = buffer.suggestion 

338 if not suggestion: 

339 return 

340 

341 query = _get_query(buffer.document) 

342 current = query + suggestion.text 

343 

344 direction_method(query=query, other_than=current, history=buffer.history) 

345 

346 new_suggestion = provider.get_suggestion(buffer, buffer.document) 

347 buffer.suggestion = new_suggestion 

348 

349 

350def swap_autosuggestion_up(event: KeyPressEvent): 

351 """Get next autosuggestion from history.""" 

352 shell = get_ipython() 

353 provider = shell.auto_suggest 

354 

355 if not isinstance(provider, NavigableAutoSuggestFromHistory): 

356 return 

357 

358 return _swap_autosuggestion( 

359 buffer=event.current_buffer, provider=provider, direction_method=provider.up 

360 ) 

361 

362 

363def swap_autosuggestion_down(event: KeyPressEvent): 

364 """Get previous autosuggestion from history.""" 

365 shell = get_ipython() 

366 provider = shell.auto_suggest 

367 

368 if not isinstance(provider, NavigableAutoSuggestFromHistory): 

369 return 

370 

371 return _swap_autosuggestion( 

372 buffer=event.current_buffer, 

373 provider=provider, 

374 direction_method=provider.down, 

375 )