vith 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,811 @@
1
+ from __future__ import print_function
2
+
3
+ import os
4
+ import re
5
+ from typing import Any, Dict, Set
6
+
7
+ from prompt_toolkit.application import run_in_terminal
8
+ from prompt_toolkit.document import Document
9
+
10
+ __all__ = (
11
+ 'has_command_handler',
12
+ 'call_command_handler',
13
+ )
14
+
15
+
16
+ COMMANDS_TO_HANDLERS: Dict[str, Any] = {} # Global mapping Vi commands to their handler.
17
+ COMMANDS_TAKING_LOCATIONS: Set[str] = set() # Name of commands that accept locations.
18
+ SET_COMMANDS: Dict[str, Any] = {} # Mapping ':set'-commands to their handler.
19
+ SET_COMMANDS_TAKING_VALUE: Set[str] = set()
20
+
21
+
22
+ _NO_WRITE_SINCE_LAST_CHANGE_TEXT = 'No write since last change (add ! to override)'
23
+ _NO_FILE_NAME = 'No file name'
24
+
25
+
26
+ def has_command_handler(command):
27
+ return command in COMMANDS_TO_HANDLERS
28
+
29
+
30
+ def call_command_handler(command, editor, variables):
31
+ """
32
+ Execute command.
33
+ """
34
+ COMMANDS_TO_HANDLERS[command](editor, variables)
35
+
36
+
37
+ def get_commands():
38
+ return COMMANDS_TO_HANDLERS.keys()
39
+
40
+
41
+ def get_commands_taking_locations():
42
+ return COMMANDS_TAKING_LOCATIONS
43
+
44
+
45
+ # Decorators
46
+
47
+
48
+ def _cmd(name):
49
+ """
50
+ Base decorator for registering commands in this namespace.
51
+ """
52
+
53
+ def decorator(func):
54
+ COMMANDS_TO_HANDLERS[name] = func
55
+ return func
56
+
57
+ return decorator
58
+
59
+
60
+ def location_cmd(name, accepts_force=False):
61
+ """
62
+ Decorator that registers a command that takes a location as (optional)
63
+ parameter.
64
+ """
65
+ COMMANDS_TAKING_LOCATIONS.add(name)
66
+
67
+ def decorator(func):
68
+ @_cmd(name)
69
+ def command_wrapper(editor, variables):
70
+ location = variables.get('location')
71
+ force = bool(variables['force'])
72
+
73
+ if force and not accepts_force:
74
+ editor.show_message('No ! allowed')
75
+ elif accepts_force:
76
+ func(editor, location, force=force)
77
+ else:
78
+ func(editor, location)
79
+
80
+ return func
81
+
82
+ return decorator
83
+
84
+
85
+ def cmd(name, accepts_force=False):
86
+ """
87
+ Decorator that registers a command that doesn't take any parameters.
88
+ """
89
+
90
+ def decorator(func):
91
+ @_cmd(name)
92
+ def command_wrapper(editor, variables):
93
+ force = bool(variables['force'])
94
+
95
+ if force and not accepts_force:
96
+ editor.show_message('No ! allowed')
97
+ elif accepts_force:
98
+ func(editor, force=force)
99
+ else:
100
+ func(editor)
101
+
102
+ return func
103
+
104
+ return decorator
105
+
106
+
107
+ def set_cmd(name, accepts_value=False):
108
+ """
109
+ Decorator that registers a ':set'-command.
110
+ """
111
+
112
+ def decorator(func):
113
+ SET_COMMANDS[name] = func
114
+ if accepts_value:
115
+ SET_COMMANDS_TAKING_VALUE.add(name)
116
+ return func
117
+
118
+ return decorator
119
+
120
+
121
+ # Actual command implementations
122
+
123
+
124
+ @_cmd('set')
125
+ def set_command_execute(editor, variables):
126
+ """
127
+ Execute a ':set' command.
128
+ """
129
+ option = variables.get('set_option')
130
+ value = variables.get('set_value')
131
+
132
+ if option in SET_COMMANDS:
133
+ # Call the correct handler.
134
+ if option in SET_COMMANDS_TAKING_VALUE:
135
+ SET_COMMANDS[option](editor, value)
136
+ else:
137
+ SET_COMMANDS[option](editor)
138
+ else:
139
+ editor.show_message('Unknown option: %s' % option)
140
+
141
+
142
+ @cmd('bn', accepts_force=True)
143
+ def _bn(editor, force=False):
144
+ """
145
+ Go to next buffer.
146
+ """
147
+ eb = editor.window_arrangement.active_editor_buffer
148
+
149
+ if not force and eb.has_unsaved_changes:
150
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
151
+ else:
152
+ editor.window_arrangement.go_to_next_buffer()
153
+
154
+
155
+ @cmd('bp', accepts_force=True)
156
+ def _bp(editor, force=False):
157
+ """
158
+ Go to previous buffer.
159
+ """
160
+ eb = editor.window_arrangement.active_editor_buffer
161
+
162
+ if not force and eb.has_unsaved_changes:
163
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
164
+ else:
165
+ editor.window_arrangement.go_to_previous_buffer()
166
+
167
+
168
+ @cmd('only')
169
+ def only(editor):
170
+ """
171
+ Keep only the current window.
172
+ """
173
+ editor.window_arrangement.keep_only_current_window()
174
+
175
+
176
+ @cmd('hide')
177
+ def hide(editor):
178
+ """
179
+ Hide the current window.
180
+ """
181
+ editor.window_arrangement.close_window()
182
+
183
+
184
+ @location_cmd('sp')
185
+ @location_cmd('split')
186
+ def horizontal_split(editor, location):
187
+ """
188
+ Split window horizontally.
189
+ """
190
+ editor.window_arrangement.hsplit(location or None)
191
+
192
+
193
+ @location_cmd('vsp')
194
+ @location_cmd('vsplit')
195
+ def vertical_split(editor, location):
196
+ """
197
+ Split window vertically.
198
+ """
199
+ editor.window_arrangement.vsplit(location or None)
200
+
201
+
202
+ @cmd('new')
203
+ def new_buffer(editor):
204
+ """
205
+ Create new buffer.
206
+ """
207
+ editor.window_arrangement.hsplit(new=True)
208
+
209
+
210
+ @cmd('vnew')
211
+ def new_vertical_buffer(editor):
212
+ """
213
+ Create new buffer, splitting vertically.
214
+ """
215
+ editor.window_arrangement.vsplit(new=True)
216
+
217
+
218
+ @location_cmd('badd')
219
+ def buffer_add(editor, location):
220
+ """
221
+ Add a new buffer.
222
+ """
223
+ editor.window_arrangement.open_buffer(location)
224
+
225
+
226
+ @cmd('buffers')
227
+ def buffer_list(editor):
228
+ """
229
+ List all buffers.
230
+ """
231
+
232
+ def handler():
233
+ wa = editor.window_arrangement
234
+ for info in wa.list_open_buffers():
235
+ char = '%' if info.is_active else ''
236
+ eb = info.editor_buffer
237
+ print(
238
+ ' %3i %-2s %-20s line %i'
239
+ % (info.index, char, eb.location, (eb.buffer.document.cursor_position_row + 1))
240
+ )
241
+ input('\nPress ENTER to continue...')
242
+
243
+ run_in_terminal(handler)
244
+
245
+
246
+ @_cmd('b')
247
+ @_cmd('buffer')
248
+ def _buffer(editor, variables):
249
+ """
250
+ Go to one of the open buffers.
251
+ """
252
+ eb = editor.window_arrangement.active_editor_buffer
253
+ force = bool(variables['force'])
254
+
255
+ buffer_name = variables.get('buffer_name')
256
+ if buffer_name:
257
+ if not force and eb.has_unsaved_changes:
258
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
259
+ else:
260
+ editor.window_arrangement.go_to_buffer(buffer_name)
261
+
262
+
263
+ @cmd('bw', accepts_force=True)
264
+ @cmd('bd', accepts_force=True)
265
+ def buffer_wipe(editor, force=False):
266
+ """
267
+ Wipe buffer.
268
+ """
269
+ eb = editor.window_arrangement.active_editor_buffer
270
+ if not force and eb.has_unsaved_changes:
271
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
272
+ else:
273
+ editor.window_arrangement.close_buffer()
274
+
275
+
276
+ @location_cmd('o', accepts_force=True)
277
+ @location_cmd('open', accepts_force=True)
278
+ @location_cmd('e', accepts_force=True)
279
+ @location_cmd('edit', accepts_force=True)
280
+ def buffer_edit(editor, location, force=False):
281
+ """
282
+ Edit new buffer.
283
+ """
284
+ if location is None:
285
+ # Edit/open without a location will reload the current file, if there are
286
+ # no changes.
287
+ eb = editor.window_arrangement.active_editor_buffer
288
+ if eb.location is None:
289
+ editor.show_message(_NO_FILE_NAME)
290
+ elif not force and eb.has_unsaved_changes:
291
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
292
+ else:
293
+ eb.reload()
294
+ else:
295
+ editor.window_arrangement.open_buffer(location, show_in_current_window=True)
296
+
297
+
298
+ @cmd('q', accepts_force=True)
299
+ @cmd('quit', accepts_force=True)
300
+ def quit(editor, all_=False, force=False):
301
+ """
302
+ Quit.
303
+ """
304
+ ebs = editor.window_arrangement.editor_buffers
305
+
306
+ # When there are buffers that have unsaved changes, show balloon.
307
+ if not force and any(eb.has_unsaved_changes for eb in ebs):
308
+ editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT)
309
+
310
+ # When there is more than one buffer open.
311
+ elif not all_ and len(ebs) > 1:
312
+ editor.show_message('%i more files to edit' % (len(ebs) - 1))
313
+
314
+ else:
315
+ editor.application.exit()
316
+
317
+
318
+ @cmd('qa', accepts_force=True)
319
+ @cmd('qall', accepts_force=True)
320
+ def quit_all(editor, force=False):
321
+ """
322
+ Quit all.
323
+ """
324
+ quit(editor, all_=True, force=force)
325
+
326
+
327
+ @location_cmd('w', accepts_force=True)
328
+ @location_cmd('write', accepts_force=True)
329
+ def write(editor, location, force=False):
330
+ """
331
+ Write file. Returns whether the buffer was written.
332
+ """
333
+ if location and not force and os.path.exists(os.path.expanduser(location)):
334
+ editor.show_message('File exists (add ! to override)')
335
+ return False
336
+ eb = editor.window_arrangement.active_editor_buffer
337
+ if location is None and eb.location is None:
338
+ editor.show_message(_NO_FILE_NAME)
339
+ return False
340
+ return eb.write(location)
341
+
342
+
343
+ @location_cmd('wq', accepts_force=True)
344
+ def write_and_quit(editor, location, force=False):
345
+ """
346
+ Write file and quit. Does not quit when the write failed.
347
+ """
348
+ if write(editor, location, force=force):
349
+ editor.application.exit()
350
+
351
+
352
+ @cmd('cq')
353
+ def quit_nonzero(editor):
354
+ """
355
+ Quit with non zero exit status.
356
+ """
357
+ # Note: the try/finally in `prompt_toolkit.Interface.read_input`
358
+ # will ensure that the render output is reset, leaving the alternate
359
+ # screen before quitting.
360
+ editor.application.exit()
361
+
362
+
363
+ @cmd('wqa')
364
+ def write_and_quit_all(editor):
365
+ """
366
+ Write current buffer and quit all.
367
+ """
368
+ eb = editor.window_arrangement.active_editor_buffer
369
+ if eb.location is None:
370
+ editor.show_message(_NO_FILE_NAME)
371
+ elif eb.write():
372
+ quit(editor, all_=True, force=False)
373
+
374
+
375
+ @cmd('h')
376
+ @cmd('help')
377
+ def help(editor):
378
+ """
379
+ Show help.
380
+ """
381
+ editor.show_help()
382
+
383
+
384
+ @location_cmd('tabe')
385
+ @location_cmd('tabedit')
386
+ @location_cmd('tabnew')
387
+ def tab_new(editor, location):
388
+ """
389
+ Create new tab page.
390
+ """
391
+ editor.window_arrangement.create_tab(location or None)
392
+
393
+
394
+ @cmd('tabclose')
395
+ @cmd('tabc')
396
+ def tab_close(editor):
397
+ """
398
+ Close tab page.
399
+ """
400
+ editor.window_arrangement.close_tab()
401
+
402
+
403
+ @cmd('tabnext')
404
+ @cmd('tabn')
405
+ def tab_next(editor):
406
+ """
407
+ Go to next tab.
408
+ """
409
+ editor.window_arrangement.go_to_next_tab()
410
+
411
+
412
+ @cmd('tabprevious')
413
+ @cmd('tabp')
414
+ def tab_previous(editor):
415
+ """
416
+ Go to previous tab.
417
+ """
418
+ editor.window_arrangement.go_to_previous_tab()
419
+
420
+
421
+ @cmd('pwd')
422
+ def pwd(editor):
423
+ "Print working directory."
424
+ directory = os.getcwd()
425
+ editor.show_message('{}'.format(directory))
426
+
427
+
428
+ @location_cmd('cd', accepts_force=False)
429
+ def cd(editor, location):
430
+ "Change working directory."
431
+ target = os.path.expanduser(location or '~')
432
+ try:
433
+ os.chdir(target)
434
+ except OSError as e:
435
+ editor.show_message('{}'.format(e))
436
+
437
+
438
+ @_cmd('colorscheme')
439
+ @_cmd('colo')
440
+ def color_scheme(editor, variables):
441
+ """
442
+ Apply a colorscheme by name.
443
+ """
444
+ colorscheme = variables.get('colorscheme')
445
+ if colorscheme:
446
+ editor.use_colorscheme(colorscheme)
447
+
448
+
449
+ @set_cmd('nu')
450
+ @set_cmd('number')
451
+ def line_numbers_show(editor):
452
+ """Show line numbers."""
453
+ editor.show_line_numbers = True
454
+
455
+
456
+ @set_cmd('nonu')
457
+ @set_cmd('nonumber')
458
+ def line_numbers_hide(editor):
459
+ """Hide line numbers."""
460
+ editor.show_line_numbers = False
461
+
462
+
463
+ @set_cmd('hlsearch')
464
+ @set_cmd('hls')
465
+ def search_highlight(editor):
466
+ """Highlight search matches."""
467
+ editor.highlight_search = True
468
+
469
+
470
+ @set_cmd('nohlsearch')
471
+ @set_cmd('nohls')
472
+ def search_no_highlight(editor):
473
+ """Don't highlight search matches."""
474
+ editor.highlight_search = False
475
+
476
+
477
+ @set_cmd('paste')
478
+ def paste_mode(editor):
479
+ """Enter paste mode."""
480
+ editor.paste_mode = True
481
+
482
+
483
+ @set_cmd('nopaste')
484
+ def paste_mode_leave(editor):
485
+ """Leave paste mode."""
486
+ editor.paste_mode = False
487
+
488
+
489
+ @set_cmd('ruler')
490
+ @set_cmd('ru')
491
+ def ruler_show(editor):
492
+ """Show ruler."""
493
+ editor.show_ruler = True
494
+
495
+
496
+ @set_cmd('noruler')
497
+ @set_cmd('noru')
498
+ def ruler_hide(editor):
499
+ """Hide ruler."""
500
+ editor.show_ruler = False
501
+
502
+
503
+ @set_cmd('wildmenu')
504
+ @set_cmd('wmnu')
505
+ def wild_menu_show(editor):
506
+ """Show wildmenu."""
507
+ editor.show_wildmenu = True
508
+
509
+
510
+ @set_cmd('nowildmenu')
511
+ @set_cmd('nowmnu')
512
+ def wild_menu_hide(editor):
513
+ """Hide wildmenu."""
514
+ editor.show_wildmenu = False
515
+
516
+
517
+ @set_cmd('expandtab')
518
+ @set_cmd('et')
519
+ def tab_expand(editor):
520
+ """Enable tab expension."""
521
+ editor.expand_tab = True
522
+
523
+
524
+ @set_cmd('noexpandtab')
525
+ @set_cmd('noet')
526
+ def tab_no_expand(editor):
527
+ """Disable tab expension."""
528
+ editor.expand_tab = False
529
+
530
+
531
+ def _set_int_option(editor, value, attribute, display_name, minimum):
532
+ """
533
+ Shared implementation of the integer-valued `:set` handlers.
534
+
535
+ `value is None` displays the current value; invalid input shows vim-like
536
+ error messages; otherwise the attribute is set when `value >= minimum`.
537
+ """
538
+ if value is None:
539
+ editor.show_message('%s=%i' % (display_name, getattr(editor, attribute)))
540
+ return
541
+ try:
542
+ number = int(value)
543
+ except ValueError:
544
+ editor.show_message('Number required after =')
545
+ return
546
+ if number < minimum:
547
+ editor.show_message('Argument must be positive')
548
+ else:
549
+ setattr(editor, attribute, number)
550
+
551
+
552
+ @set_cmd('tabstop', accepts_value=True)
553
+ @set_cmd('ts', accepts_value=True)
554
+ def tab_stop(editor, value):
555
+ """
556
+ Set tabstop.
557
+ """
558
+ _set_int_option(editor, value, 'tabstop', 'tabstop', minimum=1)
559
+
560
+
561
+ @set_cmd('scrolloff', accepts_value=True)
562
+ @set_cmd('so', accepts_value=True)
563
+ def set_scroll_offset(editor, value):
564
+ """
565
+ Set scroll offset.
566
+ """
567
+ _set_int_option(editor, value, 'scroll_offset', 'scrolloff', minimum=0)
568
+
569
+
570
+ @set_cmd('incsearch')
571
+ @set_cmd('is')
572
+ def incsearch_enable(editor):
573
+ """Enable incsearch."""
574
+ editor.incsearch = True
575
+
576
+
577
+ @set_cmd('noincsearch')
578
+ @set_cmd('nois')
579
+ def incsearch_disable(editor):
580
+ """Disable incsearch."""
581
+ editor.incsearch = False
582
+
583
+
584
+ @set_cmd('ignorecase')
585
+ @set_cmd('ic')
586
+ def search_ignorecase(editor):
587
+ """Enable case insensitive searching."""
588
+ editor.ignore_case = True
589
+
590
+
591
+ @set_cmd('noignorecase')
592
+ @set_cmd('noic')
593
+ def search_no_ignorecase(editor):
594
+ """Disable case insensitive searching."""
595
+ editor.ignore_case = False
596
+
597
+
598
+ @set_cmd('list')
599
+ def unprintable_show(editor):
600
+ """Display unprintable characters."""
601
+ editor.display_unprintable_characters = True
602
+
603
+
604
+ @set_cmd('nolist')
605
+ def unprintable_hide(editor):
606
+ """Hide unprintable characters."""
607
+ editor.display_unprintable_characters = False
608
+
609
+
610
+ @set_cmd('jedi')
611
+ def jedi_enable(editor):
612
+ """Enable Jedi autocompletion for Python files."""
613
+ editor.enable_jedi = True
614
+
615
+
616
+ @set_cmd('nojedi')
617
+ def jedi_disable(editor):
618
+ """Disable Jedi autocompletion."""
619
+ editor.enable_jedi = False
620
+
621
+
622
+ @set_cmd('relativenumber')
623
+ @set_cmd('rnu')
624
+ def relative_number(editor):
625
+ "Enable relative number"
626
+ editor.relative_number = True
627
+
628
+
629
+ @set_cmd('norelativenumber')
630
+ @set_cmd('nornu')
631
+ def no_relative_number(editor):
632
+ "Disable relative number"
633
+ editor.relative_number = False
634
+
635
+
636
+ @set_cmd('wrap')
637
+ def enable_wrap(editor):
638
+ "Enable line wrapping."
639
+ editor.wrap_lines = True
640
+
641
+
642
+ @set_cmd('nowrap')
643
+ def disable_wrap(editor):
644
+ "disable line wrapping."
645
+ editor.wrap_lines = False
646
+
647
+
648
+ @set_cmd('breakindent')
649
+ @set_cmd('bri')
650
+ def enable_breakindent(editor):
651
+ "Enable the breakindent option."
652
+ editor.break_indent = True
653
+
654
+
655
+ @set_cmd('nobreakindent')
656
+ @set_cmd('nobri')
657
+ def disable_breakindent(editor):
658
+ "Disable the breakindent option."
659
+ editor.break_indent = False
660
+
661
+
662
+ @set_cmd('mouse')
663
+ def enable_mouse(editor):
664
+ "Enable mouse ."
665
+ editor.enable_mouse_support = True
666
+
667
+
668
+ @set_cmd('nomouse')
669
+ def disable_mouse(editor):
670
+ "Disable mouse."
671
+ editor.enable_mouse_support = False
672
+
673
+
674
+ @set_cmd('tildeop')
675
+ @set_cmd('top')
676
+ def enable_tildeop(editor):
677
+ "Enable tilde operator."
678
+ editor.application.vi_state.tilde_operator = True
679
+
680
+
681
+ @set_cmd('notildeop')
682
+ @set_cmd('notop')
683
+ def disable_tildeop(editor):
684
+ "Disable tilde operator."
685
+ editor.application.vi_state.tilde_operator = False
686
+
687
+
688
+ @set_cmd('cursorline')
689
+ @set_cmd('cul')
690
+ def enable_cursorline(editor):
691
+ "Highlight the line that contains the cursor."
692
+ editor.cursorline = True
693
+
694
+
695
+ @set_cmd('nocursorline')
696
+ @set_cmd('nocul')
697
+ def disable_cursorline(editor):
698
+ "No cursorline."
699
+ editor.cursorline = False
700
+
701
+
702
+ @set_cmd('cursorcolumn')
703
+ @set_cmd('cuc')
704
+ def enable_cursorcolumn(editor):
705
+ "Highlight the column that contains the cursor."
706
+ editor.cursorcolumn = True
707
+
708
+
709
+ @set_cmd('nocursorcolumn')
710
+ @set_cmd('nocuc')
711
+ def disable_cursorcolumn(editor):
712
+ "No cursorcolumn."
713
+ editor.cursorcolumn = False
714
+
715
+
716
+ @set_cmd('colorcolumn', accepts_value=True)
717
+ @set_cmd('cc', accepts_value=True)
718
+ def set_colorcolumn(editor, value):
719
+ try:
720
+ if value:
721
+ numbers = [int(val) for val in value.split(',')]
722
+ else:
723
+ numbers = []
724
+ except ValueError:
725
+ editor.show_message('Invalid value. Expecting comma separated list of integers')
726
+ else:
727
+ editor.colorcolumn = numbers
728
+
729
+
730
+ def substitute(editor, range_start, range_end, search, replace, flags):
731
+ """Substitute /search/ with /replace/ over a range of text"""
732
+
733
+ search_state = editor.application.current_search_state
734
+ buffer = editor.current_editor_buffer.buffer
735
+ cursor_position_row = buffer.document.cursor_position_row
736
+
737
+ # Resolve search pattern from history when omitted.
738
+ if not search:
739
+ search = search_state.text
740
+ if not search:
741
+ editor.show_message('No previous regular expression')
742
+ return
743
+
744
+ if replace is None:
745
+ replace = editor.last_substitute_text
746
+
747
+ # Compile the regex once; reject malformed patterns up front.
748
+ regex_flags = re.IGNORECASE if getattr(editor, 'ignore_case', False) else 0
749
+ try:
750
+ pattern = re.compile(search, flags=regex_flags)
751
+ except re.error as e:
752
+ editor.show_message('Invalid regex: %s' % e)
753
+ return
754
+
755
+ # Convert and validate the captured range.
756
+ if not range_start:
757
+ assert not range_end
758
+ range_start_row = range_end_row = cursor_position_row
759
+ else:
760
+ range_start_row = int(range_start) - 1
761
+ range_end_row = int(range_end) - 1 if range_end else range_start_row
762
+ if not 0 <= range_start_row <= range_end_row < len(buffer.document.lines):
763
+ editor.show_message('E19: No such line in current buffer')
764
+ return
765
+ line_numbers = list(range(range_start_row, range_end_row + 1))
766
+
767
+ sub_count = 0 if 'g' in flags else 1
768
+
769
+ # Transform each line once, tracking the last substituted row and
770
+ # rejecting invalid replacement escapes without mutating the buffer.
771
+ buffer_lines = buffer.document.lines
772
+ substituted = {}
773
+ last_substituted_row = None
774
+ regex_error = None
775
+ for row in line_numbers:
776
+ original = buffer_lines[row]
777
+ try:
778
+ result = pattern.sub(replace, original, count=sub_count)
779
+ except re.error as e:
780
+ regex_error = e
781
+ break
782
+ substituted[row] = result
783
+ if result != original:
784
+ last_substituted_row = row
785
+
786
+ if regex_error is not None:
787
+ editor.show_message('Invalid regex: %s' % regex_error)
788
+ return
789
+
790
+ if last_substituted_row is None:
791
+ editor.show_message('Pattern not found')
792
+ return
793
+
794
+ # Apply substitution.
795
+ buffer.save_to_undo_stack()
796
+ lines = list(buffer.document.lines)
797
+ for row in line_numbers:
798
+ lines[row] = substituted[row]
799
+ new_text = "\n".join(lines)
800
+
801
+ doc = Document(new_text)
802
+ buffer.document = doc
803
+ row = last_substituted_row
804
+ buffer.cursor_position = doc.translate_row_col_to_index(row, 0) + (
805
+ len(doc.lines[row]) - len(doc.lines[row].lstrip())
806
+ )
807
+ buffer._search(search_state, include_current_position=True)
808
+
809
+ # update editor state
810
+ editor.last_substitute_text = replace
811
+ search_state.text = search