-
Notifications
You must be signed in to change notification settings - Fork 66
/
jove.py
1587 lines (1352 loc) · 59.3 KB
/
jove.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import re, sys, time, os
import functools as fu
import sublime, sublime_plugin
from copy import copy
from .lib.misc import *
from .lib import kill_ring
from .lib import isearch
import Default.paragraph as paragraph
from . import sbp_layout as ll
# repeatable commands
repeatable_cmds = set(['move', 'left_delete', 'right_delete', 'undo', 'redo'])
# built-in commands we need to do ensure_visible after being run
# REMIND: I think we can delete this.
built_in_ensure_visible_cmds = set(['move', 'move_to'])
class ViewWatcher(sublime_plugin.EventListener):
def __init__(self, *args, **kwargs):
super(ViewWatcher, self).__init__(*args, **kwargs)
self.pending_dedups = 0
def on_close(self, view):
ViewState.on_view_closed(view)
def on_activated(self, view):
update_pinned_status(view)
def on_deactivated(self, view):
self.disable_empty_active_mark(view)
def on_activated_async(self, view):
info = isearch.info_for(view)
if info and not view.settings().get("is_widget"):
# stop the search if we activated a new view in this window
info.done()
def on_query_context(self, view, key, operator, operand, match_all):
def test(a):
if operator == sublime.OP_EQUAL:
return a == operand
if operator == sublime.OP_NOT_EQUAL:
return a != operand
return False
if key == "i_search_active":
return test(isearch.info_for(view) is not None)
if key == "sbp_has_active_mark":
return test(CmdUtil(view).state.active_mark)
if key == "sbp_has_visible_selection":
return test(view.sel()[0].size() > 1)
if key == "sbp_use_alt_bindings":
return test(settings_helper.get("sbp_use_alt_bindings"))
if key == "sbp_use_super_bindings":
return test(settings_helper.get("sbp_use_super_bindings"))
if key == "sbp_alt+digit_inserts":
return test(settings_helper.get("sbp_alt+digit_inserts") or not settings_helper.get("sbp_use_alt_bindings"))
if key == 'sbp_has_prefix_argument':
return test(CmdUtil(view).has_prefix_arg())
if key == "sbp_catchall":
return True
def on_post_save(self, view):
# Schedule a dedup, but do not do it NOW because it seems to cause a crash if, say, we're
# saving all the buffers right now. So we schedule it for the future.
self.pending_dedups += 1
def doit():
self.pending_dedups -= 1
if self.pending_dedups == 0:
dedup_views(sublime.active_window())
sublime.set_timeout(doit, 50)
#
# Turn off active mark mode in all the views related to this view.
#
# REMIND: Sadly this is called N times for the N views that are related to the specified view,
# and then we iterator through all N views. So this is N-squared sadness, for usually 2 or fewer
# views ...
#
def on_modified(self, view):
self.disable_empty_active_mark(view, False)
def disable_empty_active_mark(self, view, must_be_empty = True):
for related_view in ViewState.most_recent_related_view(view):
util = CmdUtil(related_view)
selection = related_view.sel()
regions = list(selection)
if not must_be_empty or util.all_empty_regions(regions):
util.toggle_active_mark_mode(False)
ViewState.get(related_view).this_cmd = None
#
# CmdWatcher watches all the commands and tries to correctly process the following situations:
#
# - canceling i-search if another window command is performed or a mouse drag starts
# - override commands and run them N times if there is a numeric argument supplied
# - if transient mark mode, automatically extend the mark when using certain commands like forward
# word or character
#
class CmdWatcher(sublime_plugin.EventListener):
def __init__(self, *args, **kwargs):
super(CmdWatcher, self).__init__(*args, **kwargs)
def on_post_window_command(self, window, cmd, args):
# update_pinned_status(window.active_view())
info = isearch.info_for(window)
if info is None:
return None
# Some window commands take us to new view. Here's where we abort the isearch if that happens.
if window.active_view() != info.view:
info.done()
#
# Override some commands to execute them N times if the numeric argument is supplied.
#
def on_text_command(self, view, cmd, args):
# escape the current isearch if one is in progress, unless the command is already related to
# isearch
if isearch.info_for(view) is not None:
if cmd not in ('sbp_inc_search', 'sbp_inc_search_escape', 'drag_select'):
return ('sbp_inc_search_escape', {'next_cmd': cmd, 'next_args': args})
return
vs = ViewState.get(view)
if args is None:
args = {}
# first keep track of this_cmd and last_cmd (if command starts with "sbp_" it's handled
# elsewhere)
if not cmd.startswith("sbp_"):
vs.this_cmd = cmd
#
# Process events that create a selection. The hard part is making it work with the emacs
# region.
#
if cmd == 'drag_select':
# NOTE: This is called only when you click, NOT when you drag. So if you triple click
# it's called three times.
# NOTE: remember the view that performed the drag_select because of the
# on_selection_modified bug of using the wrong view if the same view is displayed more
# than once
self.drag_select_view = view
# cancel isearch if necessary
info = isearch.info_for(view)
if info:
info.done()
# Set drag_count to 0 when first drag_select command occurs.
if 'by' not in args:
vs.drag_count = 0
else:
self.drag_select_view = None
if cmd in ('move', 'move_to') and vs.active_mark and not args.get('extend', False):
# this is necessary or else the built-in commands (C-f, C-b) will not move when there is
# an existing selection
args['extend'] = True
return (cmd, args)
# now check for numeric argument and rewrite some commands as necessary
if not vs.argument_supplied:
return None
if cmd in repeatable_cmds:
count = vs.get_count()
args.update({
'cmd': cmd,
'_times': abs(count),
})
if count < 0 and 'forward' in args:
args['forward'] = not args['forward']
return ("sbp_do_times", args)
elif cmd == 'scroll_lines':
args['amount'] *= vs.get_count()
return (cmd, args)
#
# Post command processing: deal with active mark and resetting the numeric argument.
#
def on_post_text_command(self, view, cmd, args):
vs = ViewState.get(view)
util = CmdUtil(view)
if vs.active_mark and vs.this_cmd != 'drag_select' and vs.last_cmd == 'drag_select':
# if we just finished a mouse drag, make sure active mark mode is off
if cmd != "context_menu":
util.toggle_active_mark_mode(False)
# reset numeric argument (if command starts with "sbp_" this is handled elsewhere)
if not cmd.startswith("sbp_"):
vs.argument_value = 0
vs.argument_supplied = False
vs.last_cmd = cmd
if vs.active_mark and cmd != 'drag_select':
util.set_cursors(util.get_regions())
#
# Process the selection if it was created from a drag_select (mouse dragging) command.
#
# REMIND: This iterates all related views because sublime notifies for the same view N times, if
# there are N separate views open on the same buffer.
#
def on_selection_modified(self, active_view):
for view in ViewState.most_recent_related_view(active_view):
vs = ViewState.get(view)
selection = view.sel()
if len(selection) == 1 and vs.this_cmd == 'drag_select':
cm = CmdUtil(view, vs)
# # REMIND: we cannot rely on drag_count unfortunately because if you have the same
# # buffer in multiple views, they each get notified.
# if vs.drag_count >= 2 and not vs.active_mark:
# # wait until selection is at least 1 character long before activating
# region = view.sel()[0]
# if region.size() >= 1:
# cm.set_mark([sublime.Region(region.a, region.b)], and_selection=False)
# vs.active_mark = True
# elif vs.drag_count == 0:
# cm.toggle_active_mark_mode(False)
# vs.drag_count += 1
# update the mark ring
sel = selection[0]
vs.mark_ring.set([sublime.Region(sel.a, sel.a)], True)
class WindowCmdWatcher(sublime_plugin.EventListener):
def __init__(self, *args, **kwargs):
super(WindowCmdWatcher, self).__init__(*args, **kwargs)
def on_window_command(self, window, cmd, args):
# REMIND - JP: Why is this code here? Can't this be done in the SbpPaneCmd class?
# Check the move state of the Panes and make sure we stop recursion
if cmd == "sbp_pane_cmd" and args and args['cmd'] == 'move' and 'next_pane' not in args:
lm = ll.LayoutManager(window.layout())
if args["direction"] == 'next':
pos = lm.next(window.active_group())
else:
pos = lm.next(window.active_group(), -1)
args["next_pane"] = pos
return cmd, args
class SbpChainCommand(SbpTextCommand):
"""A command that easily runs a sequence of other commands."""
def run_cmd(self, util, commands, ensure_point_visible=False):
for c in commands:
if 'window_command' in c:
util.run_window_command(c['window_command'], c['args'])
elif 'command' in c:
util.run_command(c['command'], c['args'])
if ensure_point_visible:
util.ensure_visible(sublime.Region(util.get_point()))
#
# Calls run command a specified number of times.
#
class SbpDoTimesCommand(SbpTextCommand):
def run_cmd(self, util, cmd, _times, **args):
view = self.view
window = view.window()
visible = view.visible_region()
def doit():
# for i in range(_times):
# window.run_command(cmd, args)
# REMIND: window.run_command is much slower and I cannot remember why I used
# window.run_command...
for i in range(_times):
util.run_command(cmd, args)
if cmd in ('redo', 'undo'):
sublime.set_timeout(doit, 10)
else:
doit()
cursor = util.get_last_cursor()
if not visible.contains(cursor.b):
util.ensure_visible(cursor, True)
class SbpShowScopeCommand(SbpTextCommand):
def run_cmd(self, util, direction=1):
point = util.get_point()
name = self.view.scope_name(point)
region = self.view.extract_scope(point)
status = "%d bytes: %s" % (region.size(), name)
print(status)
util.set_status(status)
#
# Implements moving by words, emacs style.
#
class SbpMoveWordCommand(SbpTextCommand):
is_ensure_visible_cmd = True
def find_by_class_fallback(self, view, point, forward, classes, seperators):
if forward:
delta = 1
end_position = self.view.size()
if point > end_position:
point = end_position
else:
delta = -1
end_position = 0
if point < end_position:
point = end_position
while point != end_position:
if view.classify(point) & classes != 0:
return point
point += delta
return point
def find_by_class_native(self, view, point, forward, classes, separators):
return view.find_by_class(point, forward, classes, separators)
def run_cmd(self, util, direction=1):
view = self.view
separators = settings_helper.get("sbp_word_separators", default_sbp_word_separators)
# determine the direction
count = util.get_count() * direction
forward = count > 0
count = abs(count)
def call_find_by_class(point, classes, separators):
'''
This is a small wrapper that maps to the right find_by_class call
depending on the version of ST installed
'''
return self.find_by_class_native(view, point, forward, classes, separators)
def move_word0(cursor, first=False):
point = cursor.b
if forward:
if not first or not util.is_word_char(point, True, separators):
point = call_find_by_class(point, sublime.CLASS_WORD_START, separators)
point = call_find_by_class(point, sublime.CLASS_WORD_END, separators)
else:
if not first or not util.is_word_char(point, False, separators):
point = call_find_by_class(point, sublime.CLASS_WORD_END, separators)
point = call_find_by_class(point, sublime.CLASS_WORD_START, separators)
return sublime.Region(point, point)
for c in range(count):
util.for_each_cursor(move_word0, first=(c == 0))
#
# Advance to the beginning (or end if going backward) word unless already positioned at a word
# character. This can be used as setup for commands like upper/lower/capitalize words. This ignores
# the argument count.
#
class SbpMoveBackToIndentation(SbpTextCommand):
def run_cmd(self, util, direction=1):
view = self.view
def to_indentation(cursor):
start = cursor.begin()
while util.is_one_of(start, " \t"):
start += 1
return start
util.run_command("move_to", {"to": "hardbol", "extend": False})
util.for_each_cursor(to_indentation)
#
# Perform the uppercase/lowercase/capitalize commands on all the current cursors. If use_region is
# true, the command will be applied to the regions, not to words. The regions are either existing
# visible selection, OR, the emacs region(s) which might not be visible. If there are no non-empty
# regions and use_region=True, this command is a no-op.
#
class SbpChangeCaseCommand(SbpTextCommand):
re_to_underscore = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
re_to_camel = re.compile(r'(?!^)_([a-zA-Z])')
# re_to_camel = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
def underscore(self, text):
s1 = self.re_to_underscore.sub(r'_\1', text).lower()
return s1
def camel(self, text):
s1 = self.re_to_camel.sub(lambda m: m.group(1).upper(), text)
return s1
def run_cmd(self, util, mode, use_region=False, direction=1):
view = self.view
count = util.get_count(True)
# If cursors are not empty (e.g., visible marks) then we use the selection and we're in
# region mode. If the cursors are empty but the emacs regions are not, we use them as long
# as mode="regions". Otherwise, we generate regions by applying a word motion command.
selection = view.sel()
regions = list(selection)
empty_cursors = util.all_empty_regions(regions)
if empty_cursors and use_region:
emacs_regions = util.get_regions()
if emacs_regions and not util.all_empty_regions(emacs_regions):
empty_cursors = False
selection.clear()
selection.add_all(emacs_regions)
if empty_cursors:
if use_region:
return
# This works first by finding the bounds of the operation by executing a forward-word
# command. Then it performs the case command. But only if there are no selections or
# regions to operate on.
# run the move-word command so we can create a region
direction = -1 if count < 0 else 1
util.run_command("sbp_move_word", {"direction": 1})
# now the selection is at the "other end" and so we create regions out of all the
# cursors
new_regions = []
for r, s in zip(regions, selection):
new_regions.append(r.cover(s))
selection.clear()
selection.add_all(new_regions)
# perform the operation
if mode in ('upper', 'lower'):
util.run_command(mode + "_case", {})
elif mode == "title":
for r in selection:
util.view.replace(util.edit, r, view.substr(r).title())
elif mode in ("underscore", "camel"):
fcn = self.underscore if mode == "underscore" else self.camel
delta = 0
for r, s in zip(regions, selection):
orig = view.substr(s)
replace = fcn(orig)
this_delta = len(orig) - len(replace)
util.view.replace(util.edit, s, replace)
# We need to adjust the size of regions by this_delta, and the position of each
# region by the accumulated delta for when we put the selection back at the end.
if s.b > s.a:
r.b -= this_delta
else:
r.a -= this_delta
r.b -= delta
r.a -= delta
delta += this_delta
else:
print("Unknown case setting:", mode)
return
if empty_cursors and count > 0:
# was a word-based execution
for r in new_regions:
r.a = r.b = r.end()
selection.clear()
selection.add_all(new_regions)
else:
# we used the selection or the emacs regions
selection.clear()
selection.add_all(regions)
#
# A poor implementation of moving by s-expressions. The problem is it tries to use the built-in
# sublime capabilities for matching brackets, and it can be tricky getting that to work.
#
# The real solution is to figure out how to require/request the bracket highlighter code to be
# loaded and just use it.
#
class SbpMoveSexprCommand(SbpTextCommand):
is_ensure_visible_cmd = True
should_reset_target_column = True
def run_cmd(self, util, direction=1):
view = self.view
separators = settings_helper.get("sbp_sexpr_separators", default_sbp_sexpr_separators)
# determine the direction
count = util.get_count() * direction
forward = count > 0
count = abs(count)
def advance(cursor, first):
point = cursor.b
if forward:
limit = view.size()
while point < limit:
if util.is_word_char(point, True, separators):
point = view.find_by_class(point, True, sublime.CLASS_WORD_END, separators)
break
else:
ch = view.substr(point)
if ch in "({[`'\"":
next_point = util.to_other_end(point, direction)
if next_point is not None:
point = next_point
break
point += 1
else:
while point > 0:
if util.is_word_char(point, False, separators):
point = view.find_by_class(point, False, sublime.CLASS_WORD_START, separators)
break
else:
ch = view.substr(point - 1)
if ch in ")}]`'\"":
next_point = util.to_other_end(point, direction)
if next_point is not None:
point = next_point
break
point -= 1
cursor.a = cursor.b = point
return cursor
for c in range(count):
util.for_each_cursor(advance, (c == 0))
# Move to paragraph depends on the functionality provided by the default
# plugin in ST. So for now we use this.
class SbpMoveToParagraphCommand(SbpTextCommand):
def run_cmd(self, util, direction=1):
view = self.view
count = util.get_count() * direction
forward = count > 0
count = abs(count)
def advance(cursor):
whitespace = '\t\x0b\x0c\r \n'
if not forward:
# Remove whitespace and new lines for moving forward and backward paragraphs
this_region_begin = max(0, cursor.begin() - 1)
while this_region_begin > 0 and view.substr(this_region_begin) in whitespace:
this_region_begin -= 1
point = paragraph.expand_to_paragraph(view, this_region_begin).begin()
else:
this_region_end = cursor.end()
limit = self.view.size() - 1
while this_region_end < limit and view.substr(this_region_end) in whitespace:
this_region_end += 1
point = paragraph.expand_to_paragraph(self.view, this_region_end).end()
return sublime.Region(point)
for c in range(count):
util.for_each_cursor(advance)
s = view.sel()
util.ensure_visible(s[-1] if forward else s[0])
#
# A class which implements all the hard work of performing a move and then delete/kill command. It
# keeps track of the cursors, then runs the command to move all the cursors, and then performs the
# kill. This is used by the generic SbpMoveThenDeleteCommand command, but also commands that require
# input from a panel and so are not synchronous.
#
class MoveThenDeleteHelper():
def __init__(self, util):
self.util = util
self.selection = util.view.sel()
# assume forward kill direction
self.forward = True
# remember the current cursor positions
self.orig_cursors = [s for s in self.selection]
# Remember if previous was a kill command now, because if we check in self.finish() it's too
# late and the answer is always yes (because of this command we're "helping").
self.last_was_kill_cmd = util.state.last_was_kill_cmd()
#
# Finish the operation. Sometimes we're called later with a new util object, because the whole
# thing was done asynchronously (see the zap code).
#
def finish(self, new_util=None):
util = new_util if new_util else self.util
view = util.view
selection = self.selection
orig_cursors = self.orig_cursors
# extend all cursors so we can delete the bytes
new_cursors = list(selection)
# but first check to see how many regions collapsed as a result of moving the cursors (e.g.,
# if they pile up at the end of the buffer)
collapsed_regions = len(orig_cursors) - len(new_cursors)
if collapsed_regions == 0:
# OK - so now check to see how many collapse after we combine the beginning and end
# points of each region. We do that by creating the selection object, which disallows
# overlapping regions by collapsing them.
selection.clear()
for old,new in zip(orig_cursors, new_cursors):
if old < new:
selection.add(sublime.Region(old.begin(), new.end()))
else:
selection.add(sublime.Region(new.begin(), old.end()))
collapsed_regions = len(orig_cursors) - len(selection)
# OK one final check to see if any regions will overlap each other after we perform the
# kill.
if collapsed_regions == 0:
cursors = list(selection)
for i, c in enumerate(cursors[1:]):
if cursors[i].contains(c.begin()):
collapsed_regions += 1
if collapsed_regions != 0:
# restore everything to previous state and display a popup error
selection.clear()
selection.add_all(orig_cursors)
sublime.error_message("Couldn't perform kill operation because %d regions would have collapsed into adjacent regions!" % collapsed_regions)
return
# copy the text into the kill ring
regions = [view.substr(r) for r in view.sel()]
kill_ring.add(regions, forward=self.forward, join=self.last_was_kill_cmd)
# erase the regions
for region in selection:
view.erase(util.edit, region)
#
# This command remembers all the current cursor positions, executes a command on all the cursors,
# and then deletes all the data between the two.
#
class SbpMoveThenDeleteCommand(SbpTextCommand):
is_ensure_visible_cmd = True
is_kill_cmd = True
def run_cmd(self, util, move_cmd, **kwargs):
# prepare
helper = MoveThenDeleteHelper(util)
# peek at the count and update the helper's forward direction
count = util.get_count(True)
if 'direction' in kwargs:
count *= kwargs['direction']
helper.forward = count > 0
util.view.run_command(move_cmd, kwargs)
helper.finish()
#
# Goto the the Nth line as specified by the emacs arg count, or prompt for a line number of one
# isn't specified.
#
class SbpGotoLineCommand(SbpTextCommand):
is_ensure_visible_cmd = True
def run_cmd(self, util):
if util.has_prefix_arg():
util.goto_line(util.get_count())
else:
util.run_window_command("show_overlay", {"overlay": "goto", "text": ":"})
class SbpUniversalArgumentCommand(SbpTextCommand):
def run_cmd(self, util, value):
state = util.state
if not state.argument_supplied:
state.argument_supplied = True
if value == 'by_four':
state.argument_value = 4
elif value == 'negative':
state.argument_negative = True
else:
state.argument_value = value
elif value == 'by_four':
state.argument_value *= 4
elif isinstance(value, int):
state.argument_value *= 10
state.argument_value += value
elif value == 'negative':
state.argument_value = -state.argument_value
class SbpShiftRegionCommand(SbpTextCommand):
"""Shifts the emacs region left or right."""
def run_cmd(self, util, direction):
view = self.view
state = util.state
regions = util.get_regions()
if not regions:
regions = util.get_cursors()
if regions:
util.save_cursors("shift")
util.toggle_active_mark_mode(False)
selection = self.view.sel()
selection.clear()
# figure out how far we're moving
if state.argument_supplied:
cols = direction * util.get_count()
else:
cols = direction * util.get_tab_size()
# now we know which way and how far we're shifting, create a cursor for each line we
# want to shift
amount = abs(cols)
count = 0
shifted = 0
for region in regions:
for line in util.for_each_line(region):
count += 1
if cols < 0 and (line.size() < amount or not util.is_blank(line.a, line.a + amount)):
continue
selection.add(sublime.Region(line.a, line.a))
shifted += 1
# shift the region
if cols > 0:
# shift right
self.view.run_command("insert", {"characters": " " * cols})
else:
for i in range(amount):
self.view.run_command("right_delete")
# restore the region
util.restore_cursors("shift")
util.set_status("Shifted %d of %d lines in the region" % (shifted, count))
# Enum definition
def enum(**enums):
return type('Enum', (), enums)
SCROLL_TYPES = enum(TOP=1, CENTER=0, BOTTOM=2)
class SbpCenterViewCommand(SbpTextCommand):
'''
Reposition the view so that the line containing the cursor is at the
center of the viewport, if possible. Like the corresponding Emacs
command, recenter-top-bottom, this command cycles through
scrolling positions. If the prefix args are used it centers given an offset
else the cycling command is used
This command is frequently bound to Ctrl-l.
'''
last_sel = None
last_scroll_type = None
last_visible_region = None
def rowdiff(self, start, end):
r1,c1 = self.view.rowcol(start)
r2,c2 = self.view.rowcol(end)
return r2 - r1
def run_cmd(self, util, center_only=False):
view = self.view
point = util.get_point()
if util.has_prefix_arg():
lines = util.get_count()
line_height = view.line_height()
ignore, point_offy = view.text_to_layout(point)
offx, ignore = view.viewport_position()
view.set_viewport_position((offx, point_offy - line_height * lines))
elif center_only:
self.view.show_at_center(util.get_point())
else:
self.cycle_center_view(view.sel()[0])
def cycle_center_view(self, start):
if start != SbpCenterViewCommand.last_sel:
SbpCenterViewCommand.last_visible_region = None
SbpCenterViewCommand.last_scroll_type = SCROLL_TYPES.CENTER
SbpCenterViewCommand.last_sel = start
self.view.show_at_center(SbpCenterViewCommand.last_sel)
return
else:
SbpCenterViewCommand.last_scroll_type = (SbpCenterViewCommand.last_scroll_type + 1) % 3
SbpCenterViewCommand.last_sel = start
if SbpCenterViewCommand.last_visible_region == None:
SbpCenterViewCommand.last_visible_region = self.view.visible_region()
# Now Scroll to position
if SbpCenterViewCommand.last_scroll_type == SCROLL_TYPES.CENTER:
self.view.show_at_center(SbpCenterViewCommand.last_sel)
elif SbpCenterViewCommand.last_scroll_type == SCROLL_TYPES.TOP:
row,col = self.view.rowcol(SbpCenterViewCommand.last_visible_region.end())
diff = self.rowdiff(SbpCenterViewCommand.last_visible_region.begin(), SbpCenterViewCommand.last_sel.begin())
self.view.show(self.view.text_point(row + diff-2, 0), False)
elif SbpCenterViewCommand.last_scroll_type == SCROLL_TYPES.BOTTOM:
row, col = self.view.rowcol(SbpCenterViewCommand.last_visible_region.begin())
diff = self.rowdiff(SbpCenterViewCommand.last_sel.begin(), SbpCenterViewCommand.last_visible_region.end())
self.view.show(self.view.text_point(row - diff+2, 0), False)
class SbpSetMarkCommand(SbpTextCommand):
def run_cmd(self, util):
state = util.state
if state.argument_supplied:
cursors = state.mark_ring.pop()
if cursors:
util.set_cursors(cursors)
state.this_cmd = 'sbp_pop_mark'
elif state.this_cmd == state.last_cmd:
# at least two set mark commands in a row: turn ON the highlight
util.toggle_active_mark_mode()
else:
# set the mark
util.set_mark()
if settings_helper.get("sbp_active_mark_mode", False):
util.set_active_mark_mode()
class SbpCancelMarkCommand(SbpTextCommand):
def run_cmd(self, util):
if util.state.active_mark:
util.toggle_active_mark_mode()
util.state.mark_ring.clear()
class SbpSwapPointAndMarkCommand(SbpTextCommand):
def run_cmd(self, util, toggle_active_mark_mode=False):
if util.state.argument_supplied or toggle_active_mark_mode:
util.toggle_active_mark_mode()
else:
util.swap_point_and_mark()
class SbpEnableActiveMarkCommand(SbpTextCommand):
def run_cmd(self, util, enabled):
util.toggle_active_mark_mode(enabled)
class SbpMoveToCommand(SbpTextCommand):
is_ensure_visible_cmd = True
def run_cmd(self, util, to, always_push_mark=False):
if to == 'bof':
util.push_mark_and_goto_position(0)
elif to == 'eof':
util.push_mark_and_goto_position(self.view.size())
elif to in ('eow', 'bow'):
visible = self.view.visible_region()
pos = visible.a if to == 'bow' else visible.b
if always_push_mark:
util.push_mark_and_goto_position(pos)
else:
util.set_cursors([sublime.Region(pos)])
class SbpSelectAllCommand(SbpTextCommand):
def run_cmd(self, util, activate_mark=True):
# set mark at current position
util.set_mark()
# set a mark at end of file
util.set_mark(regions=[sublime.Region(self.view.size())])
# goto the top of the file
util.set_point(0)
if activate_mark:
util.toggle_active_mark_mode(True)
else:
util.ensure_visible(sublime.Region(0))
class SbpOpenLineCommand(SbpTextCommand):
def run_cmd(self, util):
view = self.view
count = util.get_count()
if count > 0:
for point in view.sel():
view.insert(util.edit, point.b, "\n" * count)
while count > 0:
view.run_command("move", {"by": "characters", "forward": False})
count -= 1
class SbpKillRegionCommand(SbpTextCommand):
is_kill_cmd = True
def run_cmd(self, util, is_copy=False):
view = self.view
regions = util.get_regions()
if regions:
data = [view.substr(r) for r in regions]
kill_ring.add(data, True, False)
if not is_copy:
for r in reversed(regions):
view.erase(util.edit, r)
else:
bytes = sum(len(d) for d in data)
util.set_status("Copied %d bytes in %d regions" % (bytes, len(data)))
util.toggle_active_mark_mode(False)
class SbpPaneCmdCommand(SbpWindowCommand):
def run_cmd(self, util, cmd, **kwargs):
if cmd == 'split':
self.split(self.window, util, **kwargs)
elif cmd == 'grow':
self.grow(self.window, util, **kwargs)
elif cmd == 'destroy':
self.destroy(self.window, **kwargs)
elif cmd in ('move', 'switch_tab'):
self.move(self.window, **kwargs)
else:
print("Unknown command")
#
# Grow the current selected window group (pane). Amount is usually 1 or -1 for grow and shrink.
#
def grow(self, window, util, direction):
if window.num_groups() == 1:
return
# Prepare the layout
layout = window.layout()
lm = ll.LayoutManager(layout)
rows = lm.rows()
cols = lm.cols()
cells = layout['cells']
# calculate the width and height in pixels of all the views
width = height = dx = dy = 0
for g,cell in enumerate(cells):
view = window.active_view_in_group(g)
w,h = view.viewport_extent()
width += w
height += h
dx += cols[cell[2]] - cols[cell[0]]
dy += rows[cell[3]] - rows[cell[1]]
width /= dx
height /= dy
current = window.active_group()
view = util.view
# Handle vertical moves
count = util.get_count()
if direction in ('g', 's'):
unit = view.line_height() / height
else:
unit = view.em_width() / width
window.set_layout(lm.extend(current, direction, unit, count))
# make sure point doesn't disappear in any active view - a delay is needed for this to work
def ensure_visible():
for g in range(window.num_groups()):
view = window.active_view_in_group(g)
util = CmdUtil(view)
util.ensure_visible(util.get_last_cursor())
sublime.set_timeout(ensure_visible, 50)
#
# Split the current pane in half. Clone the current view into the new pane. Refuses to split if
# the resulting windows would be too small.
def split(self, window, util, stype):
layout = window.layout()
current = window.active_group()
group_count = window.num_groups()
view = window.active_view()
extent = view.viewport_extent()
if stype == "h" and extent[1] / 2 <= 4 * view.line_height():
return False
if stype == "v" and extent[0] / 2 <= 20 * view.em_width():
return False
# Perform the layout
lm = ll.LayoutManager(layout)
if not lm.split(current, stype):
return False
window.set_layout(lm.build())
# couldn't find an existing view so we have to clone the current one
window.run_command("clone_file")
# the cloned view becomes the new active view
new_view = window.active_view()
# move the new view into the new group (add the end of the list)
window.set_view_index(new_view, group_count, 0)
# make sure the original view is the focus in the original pane
window.focus_view(view)
# switch to new pane
window.focus_group(group_count + 1)
# after a short delay make sure the two views are looking at the same area
def setup_views():
selection = new_view.sel()
selection.clear()
selection.add_all([r for r in view.sel()])