1# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com> 2# 3# All Rights Reserved 4# 5# 6# Permission to use, copy, modify, and distribute this software and 7# its documentation for any purpose is hereby granted without fee, 8# provided that the above copyright notice appear in all copies and 9# that both that copyright notice and this permission notice appear in 10# supporting documentation. 11# 12# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO 13# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 14# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 15# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 16# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF 17# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 18# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 20from __future__ import annotations 21 22from contextlib import contextmanager 23from dataclasses import dataclass, field 24 25from . import commands, input 26from .reader import Reader 27 28 29if False: 30 from .types import SimpleContextManager, KeySpec, CommandName 31 32 33isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple( 34 [("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"] 35 + [(c, "isearch-add-character") for c in map(chr, range(32, 127)) if c != "\\"] 36 + [ 37 ("\\%03o" % c, "isearch-add-character") 38 for c in range(256) 39 if chr(c).isalpha() and chr(c) != "\\" 40 ] 41 + [ 42 ("\\\\", "self-insert"), 43 (r"\C-r", "isearch-backwards"), 44 (r"\C-s", "isearch-forwards"), 45 (r"\C-c", "isearch-cancel"), 46 (r"\C-g", "isearch-cancel"), 47 (r"\<backspace>", "isearch-backspace"), 48 ] 49) 50 51ISEARCH_DIRECTION_NONE = "" 52ISEARCH_DIRECTION_BACKWARDS = "r" 53ISEARCH_DIRECTION_FORWARDS = "f" 54 55 56class next_history(commands.Command): 57 def do(self) -> None: 58 r = self.reader 59 if r.historyi == len(r.history): 60 r.error("end of history list") 61 return 62 r.select_item(r.historyi + 1) 63 64 65class previous_history(commands.Command): 66 def do(self) -> None: 67 r = self.reader 68 if r.historyi == 0: 69 r.error("start of history list") 70 return 71 r.select_item(r.historyi - 1) 72 73 74class history_search_backward(commands.Command): 75 def do(self) -> None: 76 r = self.reader 77 r.search_next(forwards=False) 78 79 80class history_search_forward(commands.Command): 81 def do(self) -> None: 82 r = self.reader 83 r.search_next(forwards=True) 84 85 86class restore_history(commands.Command): 87 def do(self) -> None: 88 r = self.reader 89 if r.historyi != len(r.history): 90 if r.get_unicode() != r.history[r.historyi]: 91 r.buffer = list(r.history[r.historyi]) 92 r.pos = len(r.buffer) 93 r.dirty = True 94 95 96class first_history(commands.Command): 97 def do(self) -> None: 98 self.reader.select_item(0) 99 100 101class last_history(commands.Command): 102 def do(self) -> None: 103 self.reader.select_item(len(self.reader.history)) 104 105 106class operate_and_get_next(commands.FinishCommand): 107 def do(self) -> None: 108 self.reader.next_history = self.reader.historyi + 1 109 110 111class yank_arg(commands.Command): 112 def do(self) -> None: 113 r = self.reader 114 if r.last_command is self.__class__: 115 r.yank_arg_i += 1 116 else: 117 r.yank_arg_i = 0 118 if r.historyi < r.yank_arg_i: 119 r.error("beginning of history list") 120 return 121 a = r.get_arg(-1) 122 # XXX how to split? 123 words = r.get_item(r.historyi - r.yank_arg_i - 1).split() 124 if a < -len(words) or a >= len(words): 125 r.error("no such arg") 126 return 127 w = words[a] 128 b = r.buffer 129 if r.yank_arg_i > 0: 130 o = len(r.yank_arg_yanked) 131 else: 132 o = 0 133 b[r.pos - o : r.pos] = list(w) 134 r.yank_arg_yanked = w 135 r.pos += len(w) - o 136 r.dirty = True 137 138 139class forward_history_isearch(commands.Command): 140 def do(self) -> None: 141 r = self.reader 142 r.isearch_direction = ISEARCH_DIRECTION_FORWARDS 143 r.isearch_start = r.historyi, r.pos 144 r.isearch_term = "" 145 r.dirty = True 146 r.push_input_trans(r.isearch_trans) 147 148 149class reverse_history_isearch(commands.Command): 150 def do(self) -> None: 151 r = self.reader 152 r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS 153 r.dirty = True 154 r.isearch_term = "" 155 r.push_input_trans(r.isearch_trans) 156 r.isearch_start = r.historyi, r.pos 157 158 159class isearch_cancel(commands.Command): 160 def do(self) -> None: 161 r = self.reader 162 r.isearch_direction = ISEARCH_DIRECTION_NONE 163 r.pop_input_trans() 164 r.select_item(r.isearch_start[0]) 165 r.pos = r.isearch_start[1] 166 r.dirty = True 167 168 169class isearch_add_character(commands.Command): 170 def do(self) -> None: 171 r = self.reader 172 b = r.buffer 173 r.isearch_term += self.event[-1] 174 r.dirty = True 175 p = r.pos + len(r.isearch_term) - 1 176 if b[p : p + 1] != [r.isearch_term[-1]]: 177 r.isearch_next() 178 179 180class isearch_backspace(commands.Command): 181 def do(self) -> None: 182 r = self.reader 183 if len(r.isearch_term) > 0: 184 r.isearch_term = r.isearch_term[:-1] 185 r.dirty = True 186 else: 187 r.error("nothing to rubout") 188 189 190class isearch_forwards(commands.Command): 191 def do(self) -> None: 192 r = self.reader 193 r.isearch_direction = ISEARCH_DIRECTION_FORWARDS 194 r.isearch_next() 195 196 197class isearch_backwards(commands.Command): 198 def do(self) -> None: 199 r = self.reader 200 r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS 201 r.isearch_next() 202 203 204class isearch_end(commands.Command): 205 def do(self) -> None: 206 r = self.reader 207 r.isearch_direction = ISEARCH_DIRECTION_NONE 208 r.console.forgetinput() 209 r.pop_input_trans() 210 r.dirty = True 211 212 213@dataclass 214class HistoricalReader(Reader): 215 """Adds history support (with incremental history searching) to the 216 Reader class. 217 """ 218 219 history: list[str] = field(default_factory=list) 220 historyi: int = 0 221 next_history: int | None = None 222 transient_history: dict[int, str] = field(default_factory=dict) 223 isearch_term: str = "" 224 isearch_direction: str = ISEARCH_DIRECTION_NONE 225 isearch_start: tuple[int, int] = field(init=False) 226 isearch_trans: input.KeymapTranslator = field(init=False) 227 yank_arg_i: int = 0 228 yank_arg_yanked: str = "" 229 230 def __post_init__(self) -> None: 231 super().__post_init__() 232 for c in [ 233 next_history, 234 previous_history, 235 restore_history, 236 first_history, 237 last_history, 238 yank_arg, 239 forward_history_isearch, 240 reverse_history_isearch, 241 isearch_end, 242 isearch_add_character, 243 isearch_cancel, 244 isearch_add_character, 245 isearch_backspace, 246 isearch_forwards, 247 isearch_backwards, 248 operate_and_get_next, 249 history_search_backward, 250 history_search_forward, 251 ]: 252 self.commands[c.__name__] = c 253 self.commands[c.__name__.replace("_", "-")] = c 254 self.isearch_start = self.historyi, self.pos 255 self.isearch_trans = input.KeymapTranslator( 256 isearch_keymap, invalid_cls=isearch_end, character_cls=isearch_add_character 257 ) 258 259 def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: 260 return super().collect_keymap() + ( 261 (r"\C-n", "next-history"), 262 (r"\C-p", "previous-history"), 263 (r"\C-o", "operate-and-get-next"), 264 (r"\C-r", "reverse-history-isearch"), 265 (r"\C-s", "forward-history-isearch"), 266 (r"\M-r", "restore-history"), 267 (r"\M-.", "yank-arg"), 268 (r"\<page down>", "history-search-forward"), 269 (r"\x1b[6~", "history-search-forward"), 270 (r"\<page up>", "history-search-backward"), 271 (r"\x1b[5~", "history-search-backward"), 272 ) 273 274 def select_item(self, i: int) -> None: 275 self.transient_history[self.historyi] = self.get_unicode() 276 buf = self.transient_history.get(i) 277 if buf is None: 278 buf = self.history[i].rstrip() 279 self.buffer = list(buf) 280 self.historyi = i 281 self.pos = len(self.buffer) 282 self.dirty = True 283 self.last_refresh_cache.invalidated = True 284 285 def get_item(self, i: int) -> str: 286 if i != len(self.history): 287 return self.transient_history.get(i, self.history[i]) 288 else: 289 return self.transient_history.get(i, self.get_unicode()) 290 291 @contextmanager 292 def suspend(self) -> SimpleContextManager: 293 with super().suspend(): 294 try: 295 old_history = self.history[:] 296 del self.history[:] 297 yield 298 finally: 299 self.history[:] = old_history 300 301 def prepare(self) -> None: 302 super().prepare() 303 try: 304 self.transient_history = {} 305 if self.next_history is not None and self.next_history < len(self.history): 306 self.historyi = self.next_history 307 self.buffer[:] = list(self.history[self.next_history]) 308 self.pos = len(self.buffer) 309 self.transient_history[len(self.history)] = "" 310 else: 311 self.historyi = len(self.history) 312 self.next_history = None 313 except: 314 self.restore() 315 raise 316 317 def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: 318 if cursor_on_line and self.isearch_direction != ISEARCH_DIRECTION_NONE: 319 d = "rf"[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS] 320 return "(%s-search `%s') " % (d, self.isearch_term) 321 else: 322 return super().get_prompt(lineno, cursor_on_line) 323 324 def search_next(self, *, forwards: bool) -> None: 325 """Search history for the current line contents up to the cursor. 326 327 Selects the first item found. If nothing is under the cursor, any next 328 item in history is selected. 329 """ 330 pos = self.pos 331 s = self.get_unicode() 332 history_index = self.historyi 333 334 # In multiline contexts, we're only interested in the current line. 335 nl_index = s.rfind('\n', 0, pos) 336 prefix = s[nl_index + 1:pos] 337 pos = len(prefix) 338 339 match_prefix = len(prefix) 340 len_item = 0 341 if history_index < len(self.history): 342 len_item = len(self.get_item(history_index)) 343 if len_item and pos == len_item: 344 match_prefix = False 345 elif not pos: 346 match_prefix = False 347 348 while 1: 349 if forwards: 350 out_of_bounds = history_index >= len(self.history) - 1 351 else: 352 out_of_bounds = history_index == 0 353 if out_of_bounds: 354 if forwards and not match_prefix: 355 self.pos = 0 356 self.buffer = [] 357 self.dirty = True 358 else: 359 self.error("not found") 360 return 361 362 history_index += 1 if forwards else -1 363 s = self.get_item(history_index) 364 365 if not match_prefix: 366 self.select_item(history_index) 367 return 368 369 len_acc = 0 370 for i, line in enumerate(s.splitlines(keepends=True)): 371 if line.startswith(prefix): 372 self.select_item(history_index) 373 self.pos = pos + len_acc 374 return 375 len_acc += len(line) 376 377 def isearch_next(self) -> None: 378 st = self.isearch_term 379 p = self.pos 380 i = self.historyi 381 s = self.get_unicode() 382 forwards = self.isearch_direction == ISEARCH_DIRECTION_FORWARDS 383 while 1: 384 if forwards: 385 p = s.find(st, p + 1) 386 else: 387 p = s.rfind(st, 0, p + len(st) - 1) 388 if p != -1: 389 self.select_item(i) 390 self.pos = p 391 return 392 elif (forwards and i >= len(self.history) - 1) or (not forwards and i == 0): 393 self.error("not found") 394 return 395 else: 396 if forwards: 397 i += 1 398 s = self.get_item(i) 399 p = -1 400 else: 401 i -= 1 402 s = self.get_item(i) 403 p = len(s) 404 405 def finish(self) -> None: 406 super().finish() 407 ret = self.get_unicode() 408 for i, t in self.transient_history.items(): 409 if i < len(self.history) and i != self.historyi: 410 self.history[i] = t 411 if ret and should_auto_add_history: 412 self.history.append(ret) 413 414 415should_auto_add_history = True 416