1# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com> 2# Antonio Cuni 3# Armin Rigo 4# 5# All Rights Reserved 6# 7# 8# Permission to use, copy, modify, and distribute this software and 9# its documentation for any purpose is hereby granted without fee, 10# provided that the above copyright notice appear in all copies and 11# that both that copyright notice and this permission notice appear in 12# supporting documentation. 13# 14# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO 15# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 16# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 17# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 18# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF 19# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 20# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 22from __future__ import annotations 23import os 24 25# Categories of actions: 26# killing 27# yanking 28# motion 29# editing 30# history 31# finishing 32# [completion] 33 34 35# types 36if False: 37 from .historical_reader import HistoricalReader 38 39 40class Command: 41 finish: bool = False 42 kills_digit_arg: bool = True 43 44 def __init__( 45 self, reader: HistoricalReader, event_name: str, event: list[str] 46 ) -> None: 47 # Reader should really be "any reader" but there's too much usage of 48 # HistoricalReader methods and fields in the code below for us to 49 # refactor at the moment. 50 51 self.reader = reader 52 self.event = event 53 self.event_name = event_name 54 55 def do(self) -> None: 56 pass 57 58 59class KillCommand(Command): 60 def kill_range(self, start: int, end: int) -> None: 61 if start == end: 62 return 63 r = self.reader 64 b = r.buffer 65 text = b[start:end] 66 del b[start:end] 67 if is_kill(r.last_command): 68 if start < r.pos: 69 r.kill_ring[-1] = text + r.kill_ring[-1] 70 else: 71 r.kill_ring[-1] = r.kill_ring[-1] + text 72 else: 73 r.kill_ring.append(text) 74 r.pos = start 75 r.dirty = True 76 77 78class YankCommand(Command): 79 pass 80 81 82class MotionCommand(Command): 83 pass 84 85 86class EditCommand(Command): 87 pass 88 89 90class FinishCommand(Command): 91 finish = True 92 pass 93 94 95def is_kill(command: type[Command] | None) -> bool: 96 return command is not None and issubclass(command, KillCommand) 97 98 99def is_yank(command: type[Command] | None) -> bool: 100 return command is not None and issubclass(command, YankCommand) 101 102 103# etc 104 105 106class digit_arg(Command): 107 kills_digit_arg = False 108 109 def do(self) -> None: 110 r = self.reader 111 c = self.event[-1] 112 if c == "-": 113 if r.arg is not None: 114 r.arg = -r.arg 115 else: 116 r.arg = -1 117 else: 118 d = int(c) 119 if r.arg is None: 120 r.arg = d 121 else: 122 if r.arg < 0: 123 r.arg = 10 * r.arg - d 124 else: 125 r.arg = 10 * r.arg + d 126 r.dirty = True 127 128 129class clear_screen(Command): 130 def do(self) -> None: 131 r = self.reader 132 r.console.clear() 133 r.dirty = True 134 135 136class refresh(Command): 137 def do(self) -> None: 138 self.reader.dirty = True 139 140 141class repaint(Command): 142 def do(self) -> None: 143 self.reader.dirty = True 144 self.reader.console.repaint() 145 146 147class kill_line(KillCommand): 148 def do(self) -> None: 149 r = self.reader 150 b = r.buffer 151 eol = r.eol() 152 for c in b[r.pos : eol]: 153 if not c.isspace(): 154 self.kill_range(r.pos, eol) 155 return 156 else: 157 self.kill_range(r.pos, eol + 1) 158 159 160class unix_line_discard(KillCommand): 161 def do(self) -> None: 162 r = self.reader 163 self.kill_range(r.bol(), r.pos) 164 165 166class unix_word_rubout(KillCommand): 167 def do(self) -> None: 168 r = self.reader 169 for i in range(r.get_arg()): 170 self.kill_range(r.bow(), r.pos) 171 172 173class kill_word(KillCommand): 174 def do(self) -> None: 175 r = self.reader 176 for i in range(r.get_arg()): 177 self.kill_range(r.pos, r.eow()) 178 179 180class backward_kill_word(KillCommand): 181 def do(self) -> None: 182 r = self.reader 183 for i in range(r.get_arg()): 184 self.kill_range(r.bow(), r.pos) 185 186 187class yank(YankCommand): 188 def do(self) -> None: 189 r = self.reader 190 if not r.kill_ring: 191 r.error("nothing to yank") 192 return 193 r.insert(r.kill_ring[-1]) 194 195 196class yank_pop(YankCommand): 197 def do(self) -> None: 198 r = self.reader 199 b = r.buffer 200 if not r.kill_ring: 201 r.error("nothing to yank") 202 return 203 if not is_yank(r.last_command): 204 r.error("previous command was not a yank") 205 return 206 repl = len(r.kill_ring[-1]) 207 r.kill_ring.insert(0, r.kill_ring.pop()) 208 t = r.kill_ring[-1] 209 b[r.pos - repl : r.pos] = t 210 r.pos = r.pos - repl + len(t) 211 r.dirty = True 212 213 214class interrupt(FinishCommand): 215 def do(self) -> None: 216 import signal 217 218 self.reader.console.finish() 219 self.reader.finish() 220 os.kill(os.getpid(), signal.SIGINT) 221 222 223class ctrl_c(Command): 224 def do(self) -> None: 225 self.reader.console.finish() 226 self.reader.finish() 227 raise KeyboardInterrupt 228 229 230class suspend(Command): 231 def do(self) -> None: 232 import signal 233 234 r = self.reader 235 p = r.pos 236 r.console.finish() 237 os.kill(os.getpid(), signal.SIGSTOP) 238 ## this should probably be done 239 ## in a handler for SIGCONT? 240 r.console.prepare() 241 r.pos = p 242 # r.posxy = 0, 0 # XXX this is invalid 243 r.dirty = True 244 r.console.screen = [] 245 246 247class up(MotionCommand): 248 def do(self) -> None: 249 r = self.reader 250 for _ in range(r.get_arg()): 251 x, y = r.pos2xy() 252 new_y = y - 1 253 254 if r.bol() == 0: 255 if r.historyi > 0: 256 r.select_item(r.historyi - 1) 257 return 258 r.pos = 0 259 r.error("start of buffer") 260 return 261 262 if ( 263 x 264 > ( 265 new_x := r.max_column(new_y) 266 ) # we're past the end of the previous line 267 or x == r.max_column(y) 268 and any( 269 not i.isspace() for i in r.buffer[r.bol() :] 270 ) # move between eols 271 ): 272 x = new_x 273 274 r.setpos_from_xy(x, new_y) 275 276 277class down(MotionCommand): 278 def do(self) -> None: 279 r = self.reader 280 b = r.buffer 281 for _ in range(r.get_arg()): 282 x, y = r.pos2xy() 283 new_y = y + 1 284 285 if new_y > r.max_row(): 286 if r.historyi < len(r.history): 287 r.select_item(r.historyi + 1) 288 r.pos = r.eol(0) 289 return 290 r.pos = len(b) 291 r.error("end of buffer") 292 return 293 294 if ( 295 x 296 > ( 297 new_x := r.max_column(new_y) 298 ) # we're past the end of the previous line 299 or x == r.max_column(y) 300 and any( 301 not i.isspace() for i in r.buffer[r.bol() :] 302 ) # move between eols 303 ): 304 x = new_x 305 306 r.setpos_from_xy(x, new_y) 307 308 309class left(MotionCommand): 310 def do(self) -> None: 311 r = self.reader 312 for i in range(r.get_arg()): 313 p = r.pos - 1 314 if p >= 0: 315 r.pos = p 316 else: 317 self.reader.error("start of buffer") 318 319 320class right(MotionCommand): 321 def do(self) -> None: 322 r = self.reader 323 b = r.buffer 324 for i in range(r.get_arg()): 325 p = r.pos + 1 326 if p <= len(b): 327 r.pos = p 328 else: 329 self.reader.error("end of buffer") 330 331 332class beginning_of_line(MotionCommand): 333 def do(self) -> None: 334 self.reader.pos = self.reader.bol() 335 336 337class end_of_line(MotionCommand): 338 def do(self) -> None: 339 self.reader.pos = self.reader.eol() 340 341 342class home(MotionCommand): 343 def do(self) -> None: 344 self.reader.pos = 0 345 346 347class end(MotionCommand): 348 def do(self) -> None: 349 self.reader.pos = len(self.reader.buffer) 350 351 352class forward_word(MotionCommand): 353 def do(self) -> None: 354 r = self.reader 355 for i in range(r.get_arg()): 356 r.pos = r.eow() 357 358 359class backward_word(MotionCommand): 360 def do(self) -> None: 361 r = self.reader 362 for i in range(r.get_arg()): 363 r.pos = r.bow() 364 365 366class self_insert(EditCommand): 367 def do(self) -> None: 368 r = self.reader 369 text = self.event * r.get_arg() 370 r.insert(text) 371 372 373class insert_nl(EditCommand): 374 def do(self) -> None: 375 r = self.reader 376 r.insert("\n" * r.get_arg()) 377 378 379class transpose_characters(EditCommand): 380 def do(self) -> None: 381 r = self.reader 382 b = r.buffer 383 s = r.pos - 1 384 if s < 0: 385 r.error("cannot transpose at start of buffer") 386 else: 387 if s == len(b): 388 s -= 1 389 t = min(s + r.get_arg(), len(b) - 1) 390 c = b[s] 391 del b[s] 392 b.insert(t, c) 393 r.pos = t 394 r.dirty = True 395 396 397class backspace(EditCommand): 398 def do(self) -> None: 399 r = self.reader 400 b = r.buffer 401 for i in range(r.get_arg()): 402 if r.pos > 0: 403 r.pos -= 1 404 del b[r.pos] 405 r.dirty = True 406 else: 407 self.reader.error("can't backspace at start") 408 409 410class delete(EditCommand): 411 def do(self) -> None: 412 r = self.reader 413 b = r.buffer 414 if ( 415 r.pos == 0 416 and len(b) == 0 # this is something of a hack 417 and self.event[-1] == "\004" 418 ): 419 r.update_screen() 420 r.console.finish() 421 raise EOFError 422 for i in range(r.get_arg()): 423 if r.pos != len(b): 424 del b[r.pos] 425 r.dirty = True 426 else: 427 self.reader.error("end of buffer") 428 429 430class accept(FinishCommand): 431 def do(self) -> None: 432 pass 433 434 435class help(Command): 436 def do(self) -> None: 437 import _sitebuiltins 438 439 with self.reader.suspend(): 440 self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment, call-arg] 441 442 443class invalid_key(Command): 444 def do(self) -> None: 445 pending = self.reader.console.getpending() 446 s = "".join(self.event) + pending.data 447 self.reader.error("`%r' not bound" % s) 448 449 450class invalid_command(Command): 451 def do(self) -> None: 452 s = self.event_name 453 self.reader.error("command `%s' not known" % s) 454 455 456class show_history(Command): 457 def do(self) -> None: 458 from .pager import get_pager 459 from site import gethistoryfile # type: ignore[attr-defined] 460 461 history = os.linesep.join(self.reader.history[:]) 462 with self.reader.suspend(): 463 pager = get_pager() 464 pager(history, gethistoryfile()) 465 466 467class paste_mode(Command): 468 469 def do(self) -> None: 470 self.reader.paste_mode = not self.reader.paste_mode 471 self.reader.dirty = True 472 473 474class enable_bracketed_paste(Command): 475 def do(self) -> None: 476 self.reader.paste_mode = True 477 self.reader.in_bracketed_paste = True 478 479class disable_bracketed_paste(Command): 480 def do(self) -> None: 481 self.reader.paste_mode = False 482 self.reader.in_bracketed_paste = False 483 self.reader.dirty = True 484