vimamsa 0.1.23 → 0.1.25
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.
- checksums.yaml +4 -4
- data/.dockerignore +32 -0
- data/Dockerfile +45 -0
- data/custom_example.rb +37 -11
- data/docker_cmd.sh +7 -0
- data/exe/run_tests.rb +23 -0
- data/lib/vimamsa/actions.rb +8 -0
- data/lib/vimamsa/buffer.rb +38 -47
- data/lib/vimamsa/buffer_changetext.rb +49 -12
- data/lib/vimamsa/buffer_list.rb +2 -28
- data/lib/vimamsa/conf.rb +30 -0
- data/lib/vimamsa/diff_buffer.rb +80 -32
- data/lib/vimamsa/editor.rb +54 -67
- data/lib/vimamsa/file_finder.rb +6 -2
- data/lib/vimamsa/gui.rb +247 -63
- data/lib/vimamsa/gui_file_panel.rb +1 -0
- data/lib/vimamsa/gui_func_panel.rb +127 -0
- data/lib/vimamsa/gui_menu.rb +42 -0
- data/lib/vimamsa/gui_select_window.rb +17 -6
- data/lib/vimamsa/gui_settings.rb +347 -13
- data/lib/vimamsa/gui_sourceview.rb +116 -2
- data/lib/vimamsa/gui_text.rb +0 -22
- data/lib/vimamsa/hyper_plain_text.rb +1 -0
- data/lib/vimamsa/key_actions.rb +30 -29
- data/lib/vimamsa/key_binding_tree.rb +85 -3
- data/lib/vimamsa/key_bindings_vimlike.rb +4 -0
- data/lib/vimamsa/langservp.rb +161 -7
- data/lib/vimamsa/macro.rb +54 -7
- data/lib/vimamsa/rbvma.rb +2 -0
- data/lib/vimamsa/test_framework.rb +137 -0
- data/lib/vimamsa/version.rb +1 -1
- data/modules/calculator/calculator.rb +318 -0
- data/modules/calculator/calculator_info.rb +3 -0
- data/modules/terminal/terminal.rb +140 -0
- data/modules/terminal/terminal_info.rb +3 -0
- data/run_tests.rb +89 -0
- data/styles/dark.xml +1 -1
- data/styles/molokai_edit.xml +2 -2
- data/tests/key_bindings.rb +2 -0
- data/tests/test_basic_editing.rb +86 -0
- data/tests/test_copy_paste.rb +88 -0
- data/tests/test_key_bindings.rb +152 -0
- data/tests/test_module_interface.rb +98 -0
- data/tests/test_undo.rb +201 -0
- data/vimamsa.gemspec +6 -5
- metadata +46 -14
data/lib/vimamsa/key_actions.rb
CHANGED
|
@@ -18,15 +18,6 @@ def jump_to_next_edit
|
|
|
18
18
|
buf.jump_to_next_edit
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def is_command_mode()
|
|
22
|
-
return true if vma.kbd.mode_root_state.to_s() == "C"
|
|
23
|
-
return false
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def is_visual_mode()
|
|
27
|
-
return 1 if vma.kbd.mode_root_state.to_s() == "V"
|
|
28
|
-
return 0
|
|
29
|
-
end
|
|
30
21
|
|
|
31
22
|
reg_act(:command_to_buf, proc { command_to_buf }, "Execute command, output to buffer")
|
|
32
23
|
|
|
@@ -40,7 +31,7 @@ reg_act(:cut_selection, proc { buf.delete(SELECTION) }, "Cut selection to clipbo
|
|
|
40
31
|
|
|
41
32
|
reg_act(:insert_backspace, proc { buf.selection_active? ? buf.delete(SELECTION) : buf.delete(BACKWARD_CHAR) }, "Delete backwards")
|
|
42
33
|
reg_act(:insert_select_up, proc { insert_select_move(BACKWARD_LINE) }, "Select texte upwards")
|
|
43
|
-
reg_act(:insert_select_down, proc { insert_select_move(
|
|
34
|
+
reg_act(:insert_select_down, proc { insert_select_move(FORWARD_LINE) }, "Select text downwards")
|
|
44
35
|
|
|
45
36
|
reg_act(:copy_selection, proc { buf.copy_active_selection }, "Copy selection to clipboard")
|
|
46
37
|
reg_act(:enable_debug, proc { cnf.debug = true }, "Enable debug")
|
|
@@ -59,8 +50,8 @@ reg_act(:backup_all_buffers, proc { backup_all_buffers }, "Backup all buffers",
|
|
|
59
50
|
reg_act(:e_move_forward_char, "e_move_forward_char", "Move forward", { :group => [:move, :basic] })
|
|
60
51
|
reg_act(:e_move_backward_char, "e_move_backward_char", "Move forward", { :group => [:move, :basic] })
|
|
61
52
|
# reg_act(:history_switch_backwards, proc{bufs.history_switch_backwards}, "", { :group => :file })
|
|
62
|
-
reg_act(:history_switch_backwards, proc{bufs.history_switch(-1)}, "Prev buffer", { :group => :file })
|
|
63
|
-
reg_act(:history_switch_forwards, proc{bufs.history_switch(+1)}, "Next buffer", { :group => :file })
|
|
53
|
+
reg_act(:history_switch_backwards, proc { bufs.history_switch(-1) }, "Prev buffer", { :group => :file })
|
|
54
|
+
reg_act(:history_switch_forwards, proc { bufs.history_switch(+1) }, "Next buffer", { :group => :file })
|
|
64
55
|
reg_act(:print_buffer_access_list, proc { bufs.print_by_access_time }, "Print buffers by access time", { :group => :file })
|
|
65
56
|
reg_act(:center_on_current_line, "center_on_current_line", "", { :group => :view })
|
|
66
57
|
reg_act(:run_last_macro, proc { vma.macro.run_last_macro }, "Run last recorded or executed macro", { :group => :macro })
|
|
@@ -69,6 +60,7 @@ reg_act(:jump_to_last_edit, proc { buf.jump_to_last_edit }, "Jump to last edit p
|
|
|
69
60
|
reg_act(:jump_to_random, proc { buf.jump_to_random_pos }, "")
|
|
70
61
|
reg_act(:insert_new_line, proc { buf.insert_new_line() }, "Insert new line")
|
|
71
62
|
reg_act(:show_key_bindings, proc { show_key_bindings }, "Show key bindings")
|
|
63
|
+
reg_act(:show_free_key_bindings, proc { show_free_key_bindings }, "Show available (unbound) key binding slots")
|
|
72
64
|
reg_act(:put_file_path_to_clipboard, proc { buf.put_file_path_to_clipboard }, "Put file path of current file to clipboard")
|
|
73
65
|
reg_act(:put_file_ref_to_clipboard, proc { buf.put_file_ref_to_clipboard }, "Put file ref of current file to clipboard")
|
|
74
66
|
|
|
@@ -79,6 +71,11 @@ reg_act(:set_executable, proc { buf.set_executable }, "Set current file permissi
|
|
|
79
71
|
# reg_act(:close_all_buffers, proc { bufs.close_all_buffers() }, "Close all buffers")
|
|
80
72
|
reg_act(:close_current_buffer, proc { bufs.close_current_buffer(true) }, "Close current buffer")
|
|
81
73
|
reg_act(:toggle_file_panel, proc { vma.gui.toggle_file_panel }, "Toggle file panel")
|
|
74
|
+
reg_act(:show_message_history, proc { vma.gui.show_message_history }, "Show message history")
|
|
75
|
+
reg_act(:toggle_func_panel, proc { vma.gui.toggle_func_panel }, "Toggle LSP function panel")
|
|
76
|
+
reg_act(:refresh_func_panel, proc { vma.gui.func_panel_refresh }, "Refresh LSP function panel")
|
|
77
|
+
reg_act(:git_diff_w, proc { git_diff_w }, "Show git diff -w for whole repository")
|
|
78
|
+
reg_act(:lsp_print_functions, proc { lsp_print_functions }, "LSP print functions in current file")
|
|
82
79
|
reg_act(:comment_selection, proc { buf.comment_selection }, "Comment selection")
|
|
83
80
|
reg_act(:delete_char_forward, proc { buf.delete(CURRENT_CHAR_FORWARD) }, "Delete char forward", { :group => [:edit, :basic] })
|
|
84
81
|
reg_act(:gui_file_finder, proc { vma.FileFinder.start_gui }, "Fuzzy file finder")
|
|
@@ -86,7 +83,6 @@ reg_act(:gui_file_history_finder, proc { vma.FileHistory.start_gui }, "Fuzzy fil
|
|
|
86
83
|
reg_act(:gui_search_replace, proc { gui_search_replace }, "Search and replace")
|
|
87
84
|
reg_act(:find_next, proc { $search.jump_to_next() }, "Find next")
|
|
88
85
|
|
|
89
|
-
|
|
90
86
|
reg_act(:set_style_bold, proc { buf.style_transform(:bold) }, "Set text weight to bold")
|
|
91
87
|
reg_act(:set_style_link, proc { buf.style_transform(:link) }, "Set text as link")
|
|
92
88
|
reg_act(:V_join_lines, proc { vma.buf.convert_selected_text(:joinlines) }, "Join lines")
|
|
@@ -114,37 +110,42 @@ reg_act :delete_to_word_end, proc { buf.delete2(:to_word_end) }, "Delete to file
|
|
|
114
110
|
reg_act :delete_to_next_word_start, proc { buf.delete2(:to_next_word) }, "Delete to start of next word", { :group => [:edit, :basic] }
|
|
115
111
|
reg_act :delete_to_line_start, proc { buf.delete2(:to_line_start) }, "Delete to line start", { :group => [:edit, :basic] }
|
|
116
112
|
|
|
113
|
+
reg_act(:ack_search, proc { gui_ack }, "Ack")
|
|
117
114
|
|
|
118
|
-
reg_act(:
|
|
119
|
-
|
|
120
|
-
reg_act(:
|
|
121
|
-
reg_act(:
|
|
122
|
-
reg_act(:
|
|
123
|
-
reg_act(:redo, proc {buf.redo()}, "Redo the last undone action")
|
|
124
|
-
reg_act(:undo, proc {buf.undo()}, "Undo the last action")
|
|
115
|
+
reg_act(:copy_cur_line, proc { buf.copy_line }, "Copy the current line")
|
|
116
|
+
reg_act(:paste_before_cursor, proc { buf.paste(BEFORE) }, "Paste text before the cursor")
|
|
117
|
+
reg_act(:paste_after_cursor, proc { buf.paste(AFTER) }, "Paste text after the cursor")
|
|
118
|
+
reg_act(:paste_over_after, proc { buf.paste_over(AFTER) }, "Paste over selection or after cursor")
|
|
119
|
+
reg_act(:paste_over_before, proc { buf.paste_over(BEFORE) }, "Paste over selection or before cursor")
|
|
120
|
+
reg_act(:redo, proc { buf.redo() }, "Redo the last undone action")
|
|
121
|
+
reg_act(:undo, proc { buf.undo() }, "Undo the last action")
|
|
125
122
|
reg_act(:jump_end_of_line, proc { buf.jump(END_OF_LINE) }, "Move to the end of the current line")
|
|
126
|
-
reg_act(:jump_end_of_buffer, proc {buf.jump(END_OF_BUFFER)}, "Move to the end of the buffer")
|
|
123
|
+
reg_act(:jump_end_of_buffer, proc { buf.jump(END_OF_BUFFER) }, "Move to the end of the buffer")
|
|
127
124
|
reg_act(:jump_start_of_buffer, proc { buf.jump(START_OF_BUFFER) }, "Move to the start of the buffer")
|
|
128
125
|
reg_act(:jump_beginning_of_line, proc { buf.jump(BEGINNING_OF_LINE) }, "Move to the beginning of the current line")
|
|
129
|
-
reg_act(:jump_next_word_end, proc { buf.jump_word(FORWARD,WORD_END) }, "Jump to the end of the next word")
|
|
130
|
-
reg_act(:jump_prev_word_start, proc { buf.jump_word(BACKWARD,WORD_START) }, "Jump to the start of the previous word")
|
|
131
|
-
reg_act(:jump_next_word_start, proc { buf.jump_word(FORWARD,WORD_START) }, "Jump to the start of the next word")
|
|
126
|
+
reg_act(:jump_next_word_end, proc { buf.jump_word(FORWARD, WORD_END) }, "Jump to the end of the next word")
|
|
127
|
+
reg_act(:jump_prev_word_start, proc { buf.jump_word(BACKWARD, WORD_START) }, "Jump to the start of the previous word")
|
|
128
|
+
reg_act(:jump_next_word_start, proc { buf.jump_word(FORWARD, WORD_START) }, "Jump to the start of the next word")
|
|
132
129
|
reg_act(:insert_mode, proc { vma.kbd.set_mode(:insert) }, "Switch to INSERT mode")
|
|
133
130
|
reg_act(:prev_mode, proc { vma.kbd.to_previous_mode }, "Return to the previous mode")
|
|
134
131
|
reg_act(:move_prev_line, proc { buf.move(BACKWARD_LINE) }, "Move the cursor to the previous line")
|
|
135
132
|
reg_act(:move_next_line, proc { buf.move(FORWARD_LINE) }, "Move the cursor to the next line")
|
|
136
133
|
reg_act(:move_backward_char, proc { buf.move(BACKWARD_CHAR) }, "Move one character backward")
|
|
137
|
-
reg_act(:start_visual_mode, proc { buf.start_selection;vma.kbd.set_mode(:visual) }, "Enter VISUAL mode (for selections)")
|
|
134
|
+
reg_act(:start_visual_mode, proc { buf.start_selection; vma.kbd.set_mode(:visual) }, "Enter VISUAL mode (for selections)")
|
|
138
135
|
reg_act(:jump_last_edit, proc { buf.jump_to_last_edit }, "Jump to the last edit location")
|
|
139
136
|
reg_act(:install_demo_files, proc { install_demo_files }, "Install and show Demo")
|
|
140
137
|
reg_act(:reload_customrb, proc { reload_customrb }, "Reload custom.rb")
|
|
141
138
|
|
|
142
|
-
|
|
143
139
|
reg_act :start_browse_mode, proc {
|
|
144
140
|
vma.kbd.set_mode(:browse)
|
|
145
141
|
bufs.reset_navigation
|
|
146
142
|
}, "Start browse mode"
|
|
147
143
|
reg_act :kbd_dump_state, proc { vma.kbd.dump_state }, "Dump keyboard tree state"
|
|
144
|
+
reg_act :toggle_kbd_passthrough, proc {
|
|
145
|
+
vma.gui.instance_variable_set(:@kbd_passthrough, !vma.gui.instance_variable_get(:@kbd_passthrough))
|
|
146
|
+
state = vma.gui.instance_variable_get(:@kbd_passthrough) ? "ON" : "OFF"
|
|
147
|
+
message("Keyboard passthrough: #{state}")
|
|
148
|
+
}, "Toggle keyboard event passthrough (allow other widgets to receive key events)"
|
|
148
149
|
|
|
149
150
|
reg_act :exit_browse_mode, proc {
|
|
150
151
|
bufs.add_current_buf_to_history
|
|
@@ -207,9 +208,9 @@ act_list = {
|
|
|
207
208
|
|
|
208
209
|
:backward_line => { :proc => proc { buf.move(BACKWARD_LINE) },
|
|
209
210
|
:desc => "Move one line backward", :group => [:move, :basic] },
|
|
210
|
-
|
|
211
|
-
:increment_word => { :proc => proc { buf.increment_current_word},
|
|
212
|
-
|
|
211
|
+
|
|
212
|
+
:increment_word => { :proc => proc { buf.increment_current_word },
|
|
213
|
+
:desc => "Increment word", :group => [:edit, :extra] },
|
|
213
214
|
|
|
214
215
|
# { :proc => proc { },
|
|
215
216
|
# :desc => "", :group => : },
|
|
@@ -69,9 +69,10 @@ class KeyBindingTree
|
|
|
69
69
|
|
|
70
70
|
def set_mode(label)
|
|
71
71
|
return if get_mode == :label
|
|
72
|
+
vma.buf&.new_undo_group
|
|
72
73
|
@match_state = [@modes[label]] # used for matching input
|
|
73
74
|
@mode_root_state = @modes[label]
|
|
74
|
-
|
|
75
|
+
|
|
75
76
|
#TODO: should not happen? @default_mode_stack[-1] should be always the same as get_mode ?
|
|
76
77
|
@default_mode_stack << label if label != @default_mode_stack[-1]
|
|
77
78
|
|
|
@@ -279,6 +280,61 @@ class KeyBindingTree
|
|
|
279
280
|
@state_trail = [@mode_root_state]
|
|
280
281
|
end
|
|
281
282
|
|
|
283
|
+
# Returns a list of key positions that are free (no action bound) under each
|
|
284
|
+
# existing node in the tree. For each intermediate node (chord prefix),
|
|
285
|
+
# shows which common keys are not yet bound as its children.
|
|
286
|
+
# Does NOT recurse into free positions — only checks children of existing nodes.
|
|
287
|
+
def get_free_bindings(modes: [])
|
|
288
|
+
common_keys = ("a".."z").to_a + %w[
|
|
289
|
+
space return esc tab backspace
|
|
290
|
+
, . / ; ' [ ] \\ `
|
|
291
|
+
]
|
|
292
|
+
# delete ! @ # $ % ^ & * ( ) : " < > ?
|
|
293
|
+
# left right up down home end pageup pagedown
|
|
294
|
+
# 0 1 2 3 4 5 6 7 8 9
|
|
295
|
+
# ctrl-a ctrl-b ctrl-d ctrl-e ctrl-f ctrl-g ctrl-h
|
|
296
|
+
# ctrl-j ctrl-k ctrl-l ctrl-n ctrl-o ctrl-p ctrl-q ctrl-r
|
|
297
|
+
# ctrl-s ctrl-t ctrl-u ctrl-v ctrl-w ctrl-x ctrl-y ctrl-z
|
|
298
|
+
# shift-left shift-right shift-up shift-down
|
|
299
|
+
# F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12
|
|
300
|
+
|
|
301
|
+
lines = []
|
|
302
|
+
stack = [[@root, ""]]
|
|
303
|
+
|
|
304
|
+
while stack.any?
|
|
305
|
+
t, p = *stack.pop
|
|
306
|
+
next unless t.children.any?
|
|
307
|
+
|
|
308
|
+
t.children.each { |c|
|
|
309
|
+
if c.level == 1 && !modes.empty?
|
|
310
|
+
next unless modes.include?(c.key_name)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
if c.level == 1
|
|
314
|
+
new_p = "[#{c.key_name}]"
|
|
315
|
+
elsif c.eval_rule.size > 0
|
|
316
|
+
new_p = "#{p} #{c.key_name}(#{c.eval_rule})"
|
|
317
|
+
else
|
|
318
|
+
new_p = p.empty? ? c.key_name.to_s : "#{p} #{c.key_name}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
next unless c.children.any?
|
|
322
|
+
next if c.children.any? { |gc| gc.key_name == "<char>" }
|
|
323
|
+
|
|
324
|
+
bound = c.children.map(&:key_name).to_set
|
|
325
|
+
free = common_keys.reject { |k| bound.include?(k) }
|
|
326
|
+
lines << "#{new_p} : #{free.join(" ")}" if free.any?
|
|
327
|
+
|
|
328
|
+
stack << [c, new_p]
|
|
329
|
+
}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
lines.sort_by { |l|
|
|
333
|
+
prefix = l.split(" : ").first.split
|
|
334
|
+
[prefix.first, prefix.size, l]
|
|
335
|
+
}.join("\n")
|
|
336
|
+
end
|
|
337
|
+
|
|
282
338
|
def get_by_keywords(modes: [], keywords: [])
|
|
283
339
|
s = ""
|
|
284
340
|
stack = [[@root, ""]]
|
|
@@ -577,6 +633,14 @@ class KeyBindingTree
|
|
|
577
633
|
key.each { |k| _bindkey(k, a, keywords: keywords) }
|
|
578
634
|
end
|
|
579
635
|
|
|
636
|
+
def unbindkey(key)
|
|
637
|
+
if key.class != Array
|
|
638
|
+
key = key.split("||")
|
|
639
|
+
end
|
|
640
|
+
#TODO: test
|
|
641
|
+
key.each { |k| _bindkey(k.strip, :delete_state) }
|
|
642
|
+
end
|
|
643
|
+
|
|
580
644
|
def _bindkey(key, action, keywords: [])
|
|
581
645
|
key.strip!
|
|
582
646
|
key.gsub!(/\s+/, " ")
|
|
@@ -729,13 +793,12 @@ class KeyBindingTree
|
|
|
729
793
|
@next_command_count = nil
|
|
730
794
|
end
|
|
731
795
|
|
|
732
|
-
if cnf.kbd.show_prev_action? and trail_str.class==String
|
|
796
|
+
if cnf.kbd.show_prev_action? and trail_str.class == String
|
|
733
797
|
len_limit = 35
|
|
734
798
|
action_desc = "UNK"
|
|
735
799
|
if action.class == String && (m = action.match(/\Abuf\.insert_txt\((.+)\)\z/))
|
|
736
800
|
char_part = m[1].gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
737
801
|
action_desc = "insert #{char_part}"
|
|
738
|
-
puts action_desc.inspect
|
|
739
802
|
else
|
|
740
803
|
action_desc = vma.actions[action]&.method_name || action.to_s
|
|
741
804
|
action_desc = action_desc[0..len_limit] if action_desc.size > len_limit
|
|
@@ -750,6 +813,10 @@ def bindkey(key, action, keywords: "")
|
|
|
750
813
|
vma.kbd.bindkey(key, action, keywords: keywords)
|
|
751
814
|
end
|
|
752
815
|
|
|
816
|
+
def unbindkey(key)
|
|
817
|
+
vma.kbd.unbindkey(key)
|
|
818
|
+
end
|
|
819
|
+
|
|
753
820
|
def add_keys(keywords, to_add)
|
|
754
821
|
to_add.each { |key, value|
|
|
755
822
|
bindkey(key, value, keywords: keywords)
|
|
@@ -763,11 +830,26 @@ def exec_action(action)
|
|
|
763
830
|
return call_action(action)
|
|
764
831
|
elsif action.class == Proc
|
|
765
832
|
return action.call
|
|
833
|
+
elsif action.class == String && vma.actions.include?(action.to_sym)
|
|
834
|
+
# Symbols serialised through JSON become plain strings; dispatch them as actions.
|
|
835
|
+
return call_action(action.to_sym)
|
|
766
836
|
else
|
|
767
837
|
return eval(action)
|
|
768
838
|
end
|
|
769
839
|
end
|
|
770
840
|
|
|
841
|
+
def show_free_key_bindings()
|
|
842
|
+
kbd_s = "❙Free key binding slots❙\n"
|
|
843
|
+
kbd_s << "\n⦁[Mode] <prefix> : <free keys>⦁\n"
|
|
844
|
+
kbd_s << "[B]=Browse, [C]=Command, [I]=Insert, [V]=Visual\n"
|
|
845
|
+
kbd_s << "Free = not yet bound under that prefix\n"
|
|
846
|
+
kbd_s << "===============================================\n"
|
|
847
|
+
kbd_s << vma.kbd.get_free_bindings
|
|
848
|
+
kbd_s << "\n"
|
|
849
|
+
b = create_new_buffer(kbd_s, "free-key-bindings")
|
|
850
|
+
gui_set_file_lang(b.id, "hyperplaintext")
|
|
851
|
+
end
|
|
852
|
+
|
|
771
853
|
def show_key_bindings()
|
|
772
854
|
kbd_s = "❙Key bindings❙\n"
|
|
773
855
|
kbd_s << "\n⦁[Mode] <keys> : <action>⦁\n"
|
|
@@ -131,6 +131,7 @@ add_keys "core", {
|
|
|
131
131
|
"I enter" => :insert_new_line,
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
"I shift-left" => "insert_select_move(BACKWARD_CHAR)",
|
|
134
135
|
"I shift-right" => "insert_select_move(FORWARD_CHAR)",
|
|
135
136
|
"I shift-down" => "insert_select_move(FORWARD_LINE)",
|
|
136
137
|
"I shift-pagedown" => "insert_select_move(:pagedown)",
|
|
@@ -243,6 +244,8 @@ add_keys "core", {
|
|
|
243
244
|
|
|
244
245
|
# Replace mode
|
|
245
246
|
"X esc || X ctrl!" => "vma.kbd.to_previous_mode",
|
|
247
|
+
# "X p" => :paste_over_after, #TODO
|
|
248
|
+
"X ctrl-v" => :paste_over_before,
|
|
246
249
|
"X <char>" => "buf.replace_with_char(<char>);buf.move(FORWARD_CHAR)",
|
|
247
250
|
|
|
248
251
|
# Macros
|
|
@@ -265,6 +268,7 @@ add_keys "core", {
|
|
|
265
268
|
"C @ <char>" => "vma.macro.run_macro(<char>)",
|
|
266
269
|
"C , m S" => 'vma.macro.save_macro("a")',
|
|
267
270
|
"C , m s" => "vma.macro.save",
|
|
271
|
+
"C , m h" => :show_message_history,
|
|
268
272
|
|
|
269
273
|
# "C ." => "repeat_last_action", # TODO
|
|
270
274
|
"VC ;" => "repeat_last_find",
|
data/lib/vimamsa/langservp.rb
CHANGED
|
@@ -90,7 +90,7 @@ class LangSrv
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def handle_delta(delta, fpath, version)
|
|
93
|
-
fpuri =
|
|
93
|
+
fpuri = file_uri(fpath)
|
|
94
94
|
|
|
95
95
|
# delta[0]: char position
|
|
96
96
|
# delta[1]: INSERT or DELETE
|
|
@@ -147,7 +147,8 @@ class LangSrv
|
|
|
147
147
|
# r = @resp.delete_at(0)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
def get_definition(
|
|
150
|
+
def get_definition(fpath_or_uri, lpos, cpos)
|
|
151
|
+
fpuri = fpath_or_uri.start_with?("file://") ? fpath_or_uri : file_uri(fpath_or_uri)
|
|
151
152
|
a = LSP::Interface::DefinitionParams.new(
|
|
152
153
|
position: LSP::Interface::Position.new(line: lpos, character: cpos),
|
|
153
154
|
text_document: LSP::Interface::TextDocumentIdentifier.new(uri: fpuri),
|
|
@@ -172,22 +173,175 @@ class LangSrv
|
|
|
172
173
|
return nil
|
|
173
174
|
end
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
# LSP SymbolKind values for callable things
|
|
177
|
+
FUNCTION_KINDS = [6, 9, 12].freeze # Function, Constructor, Method
|
|
178
|
+
CLASS_KINDS = [5, 10, 11, 23].freeze # Class, Module, Interface, Namespace
|
|
179
|
+
|
|
180
|
+
# Recursively collect symbols of function kinds.
|
|
181
|
+
# Handles both flat SymbolInformation[] and nested DocumentSymbol[] (with :children).
|
|
182
|
+
def collect_functions(symbols)
|
|
183
|
+
result = []
|
|
184
|
+
symbols.each do |s|
|
|
185
|
+
result << s if FUNCTION_KINDS.include?(s[:kind])
|
|
186
|
+
result.concat(collect_functions(s[:children])) if s[:children].is_a?(Array)
|
|
187
|
+
end
|
|
188
|
+
result
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Flatten all FUNCTION_KINDS from a symbol subtree into [{name:, line:}, ...].
|
|
192
|
+
def flatten_functions(symbols)
|
|
193
|
+
result = []
|
|
194
|
+
symbols.each do |s|
|
|
195
|
+
if FUNCTION_KINDS.include?(s[:kind])
|
|
196
|
+
line = s.dig(:range, :start, :line)
|
|
197
|
+
result << { name: s[:name], line: line ? line + 1 : 0 }
|
|
198
|
+
end
|
|
199
|
+
result.concat(flatten_functions(s[:children])) if s[:children].is_a?(Array)
|
|
200
|
+
end
|
|
201
|
+
result
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Build groups from a nested DocumentSymbol[].
|
|
205
|
+
# Returns [{name:, line:, functions: [{name:, line:}, ...]}, ...]
|
|
206
|
+
# name: nil = top-level (ungrouped) functions.
|
|
207
|
+
def collect_groups_nested(symbols)
|
|
208
|
+
groups = []
|
|
209
|
+
top_funcs = []
|
|
210
|
+
symbols.each do |s|
|
|
211
|
+
if CLASS_KINDS.include?(s[:kind])
|
|
212
|
+
line = s.dig(:range, :start, :line)
|
|
213
|
+
funcs = s[:children].is_a?(Array) ? flatten_functions(s[:children]) : []
|
|
214
|
+
groups << { name: s[:name], line: line ? line + 1 : 0, functions: funcs }
|
|
215
|
+
elsif FUNCTION_KINDS.include?(s[:kind])
|
|
216
|
+
line = s.dig(:range, :start, :line)
|
|
217
|
+
top_funcs << { name: s[:name], line: line ? line + 1 : 0 }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
groups.unshift({ name: nil, line: nil, functions: top_funcs }) unless top_funcs.empty?
|
|
221
|
+
groups
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Build groups from a flat SymbolInformation[] using :containerName.
|
|
225
|
+
def collect_groups_flat(symbols)
|
|
226
|
+
|
|
227
|
+
by_container = {}
|
|
228
|
+
symbols.each do |s|
|
|
229
|
+
next unless FUNCTION_KINDS.include?(s[:kind])
|
|
230
|
+
container = s[:containerName] || ""
|
|
231
|
+
line = s.dig(:location, :range, :start, :line)
|
|
232
|
+
by_container[container] ||= []
|
|
233
|
+
by_container[container] << { name: s[:name], line: line ? line + 1 : 0 }
|
|
234
|
+
end
|
|
235
|
+
groups = []
|
|
236
|
+
top_funcs = by_container.delete("") || []
|
|
237
|
+
by_container.keys.sort.each do |container|
|
|
238
|
+
groups << { name: container, line: 0, functions: by_container[container] }
|
|
239
|
+
end
|
|
240
|
+
groups << { name: nil, line: nil, functions: top_funcs } unless top_funcs.empty?
|
|
241
|
+
|
|
242
|
+
groups
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Send textDocument/documentSymbol and return [{name:, line:}, ...] for functions/methods.
|
|
246
|
+
# Returns nil on error, empty array if no functions found.
|
|
247
|
+
def document_functions(fpath)
|
|
248
|
+
ensure_file_open(fpath)
|
|
249
|
+
fpuri = file_uri(fpath)
|
|
250
|
+
a = LSP::Interface::DocumentSymbolParams.new(
|
|
251
|
+
text_document: LSP::Interface::TextDocumentIdentifier.new(uri: fpuri),
|
|
252
|
+
)
|
|
253
|
+
id = new_id
|
|
254
|
+
@writer.write(id: id, params: a, method: "textDocument/documentSymbol")
|
|
255
|
+
r = wait_for_response(id)
|
|
256
|
+
return nil if r.nil?
|
|
257
|
+
|
|
258
|
+
symbols = r[:result]
|
|
259
|
+
return nil if !symbols.is_a?(Array)
|
|
260
|
+
|
|
261
|
+
functions = collect_functions(symbols)
|
|
262
|
+
functions.map do |s|
|
|
263
|
+
line = s.dig(:range, :start, :line) || s.dig(:location, :range, :start, :line)
|
|
264
|
+
{ name: s[:name], line: line ? line + 1 : 0 }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Like document_functions but returns functions grouped by class/module.
|
|
269
|
+
# Returns [{name:, line:, functions: [{name:, line:}, ...]}, ...]
|
|
270
|
+
# name: nil means top-level (ungrouped) functions.
|
|
271
|
+
def document_functions_grouped(fpath)
|
|
272
|
+
ensure_file_open(fpath)
|
|
273
|
+
fpuri = file_uri(fpath)
|
|
274
|
+
a = LSP::Interface::DocumentSymbolParams.new(
|
|
275
|
+
text_document: LSP::Interface::TextDocumentIdentifier.new(uri: fpuri),
|
|
276
|
+
)
|
|
277
|
+
id = new_id
|
|
278
|
+
@writer.write(id: id, params: a, method: "textDocument/documentSymbol")
|
|
279
|
+
r = wait_for_response(id)
|
|
280
|
+
return nil if r.nil?
|
|
281
|
+
|
|
282
|
+
symbols = r[:result]
|
|
283
|
+
return nil unless symbols.is_a?(Array)
|
|
284
|
+
|
|
285
|
+
# Detect nested (DocumentSymbol) vs flat (SymbolInformation) format
|
|
286
|
+
if symbols.any? { |s| s.key?(:children) }
|
|
287
|
+
collect_groups_nested(symbols)
|
|
288
|
+
else
|
|
289
|
+
collect_groups_flat(symbols)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Send textDocument/documentSymbol, filter to functions/methods, and puts them.
|
|
294
|
+
def print_functions(fpath)
|
|
295
|
+
funcs = document_functions(fpath)
|
|
296
|
+
if funcs.nil? || funcs.empty?
|
|
297
|
+
puts "(no functions found in #{File.basename(fpath)})"
|
|
298
|
+
return
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
puts "=== Functions in #{File.basename(fpath)} ==="
|
|
302
|
+
funcs.each do |f|
|
|
303
|
+
line_str = f[:line] > 0 ? ":#{f[:line]}" : ""
|
|
304
|
+
puts " #{f[:name]}#{line_str}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def file_uri(fp)
|
|
309
|
+
URI.join("file:///", fp).to_s
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def open_file(fp, fc = nil, lang: nil)
|
|
176
313
|
debug "open_file", 2
|
|
314
|
+
@opened_files ||= {}
|
|
315
|
+
fpuri = file_uri(fp)
|
|
177
316
|
fc = IO.read(fp) if fc.nil?
|
|
178
|
-
|
|
179
|
-
encoded_filepath = URI.encode_www_form_component(fp)
|
|
180
|
-
fpuri = URI.parse("file://#{encoded_filepath}")
|
|
317
|
+
lang ||= @lang
|
|
181
318
|
|
|
182
319
|
a = LSP::Interface::DidOpenTextDocumentParams.new(
|
|
183
320
|
text_document: LSP::Interface::TextDocumentItem.new(
|
|
184
321
|
uri: fpuri,
|
|
185
322
|
text: fc,
|
|
186
|
-
language_id:
|
|
323
|
+
language_id: lang,
|
|
187
324
|
version: 1,
|
|
188
325
|
),
|
|
189
326
|
)
|
|
190
327
|
|
|
191
328
|
@writer.write(method: "textDocument/didOpen", params: a)
|
|
329
|
+
@opened_files[fpuri] = true
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def ensure_file_open(fp, fc = nil)
|
|
333
|
+
@opened_files ||= {}
|
|
334
|
+
fpuri = file_uri(fp)
|
|
335
|
+
open_file(fp, fc) unless @opened_files[fpuri]
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def lsp_print_functions
|
|
340
|
+
return unless vma.buf&.fname
|
|
341
|
+
lsp = LangSrv.get(vma.buf.lang)
|
|
342
|
+
if lsp.nil?
|
|
343
|
+
message("No LSP server available for #{vma.buf.lang}")
|
|
344
|
+
return
|
|
192
345
|
end
|
|
346
|
+
lsp.print_functions(vma.buf.fname)
|
|
193
347
|
end
|
data/lib/vimamsa/macro.rb
CHANGED
|
@@ -26,22 +26,51 @@ class Macro
|
|
|
26
26
|
attr_reader :running_macro
|
|
27
27
|
attr_accessor :recorded_macros, :recording, :named_macros, :last_macro
|
|
28
28
|
|
|
29
|
+
NAMED_MACROS_FILE = "named_macros.json"
|
|
30
|
+
|
|
29
31
|
def initialize()
|
|
30
32
|
@recording = false
|
|
31
|
-
# @recorded_macros = {}
|
|
32
33
|
@current_recording = []
|
|
33
34
|
@current_name = nil
|
|
34
35
|
@last_macro = "a"
|
|
35
36
|
@running_macro = false
|
|
36
37
|
|
|
37
|
-
#TODO:
|
|
38
38
|
@recorded_macros = vma.marshal_load("macros", {})
|
|
39
|
-
@named_macros =
|
|
39
|
+
@named_macros = load_named_macros
|
|
40
40
|
vma.hook.register(:shutdown, self.method("save"))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def named_macros_path
|
|
44
|
+
get_dot_path(NAMED_MACROS_FILE)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Save named macros as JSON immediately — called automatically after name_macro.
|
|
48
|
+
def save_named_macros
|
|
49
|
+
require "json"
|
|
50
|
+
File.write(named_macros_path, JSON.pretty_generate(@named_macros))
|
|
51
|
+
rescue => e
|
|
52
|
+
error("Failed to save named macros: #{e}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Load named macros from JSON. Falls back to Marshal data from older versions.
|
|
56
|
+
def load_named_macros
|
|
57
|
+
require "json"
|
|
58
|
+
path = named_macros_path
|
|
59
|
+
if File.exist?(path)
|
|
60
|
+
data = JSON.parse(File.read(path))
|
|
61
|
+
# JSON keys are always strings; action lists are arrays of strings — correct types
|
|
62
|
+
return data
|
|
63
|
+
end
|
|
64
|
+
# Fallback: migrate from old Marshal-based storage
|
|
65
|
+
vma.marshal_load("named_macros", {})
|
|
66
|
+
rescue => e
|
|
67
|
+
error("Failed to load named macros: #{e}")
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
def save()
|
|
44
72
|
vma.marshal_save("macros", @recorded_macros)
|
|
73
|
+
# named_macros are kept current via save_named_macros; save a Marshal copy as backup
|
|
45
74
|
vma.marshal_save("named_macros", @named_macros)
|
|
46
75
|
end
|
|
47
76
|
|
|
@@ -56,17 +85,31 @@ class Macro
|
|
|
56
85
|
$macro_search_list = l
|
|
57
86
|
$select_keys = ["h", "l", "f", "d", "s", "a", "g", "z"]
|
|
58
87
|
|
|
88
|
+
delete_cb = proc { |name, refresh|
|
|
89
|
+
Gui.confirm("Delete macro '#{name}'?", proc {
|
|
90
|
+
vma.macro.delete_named_macro(name)
|
|
91
|
+
refresh.call
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
gui_select_update_window(l, $select_keys.collect { |x| x.upcase },
|
|
60
96
|
"gui_find_macro_select_callback",
|
|
61
|
-
"gui_find_macro_update_callback"
|
|
97
|
+
"gui_find_macro_update_callback",
|
|
98
|
+
{ delete_callback: delete_cb })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def delete_named_macro(name)
|
|
102
|
+
@named_macros.delete(name)
|
|
103
|
+
save_named_macros
|
|
104
|
+
message("Macro '#{name}' deleted")
|
|
62
105
|
end
|
|
63
106
|
|
|
64
107
|
def name_macro(name, id = nil)
|
|
65
108
|
debug "NAME MACRO #{name}"
|
|
66
|
-
if id.nil?
|
|
67
|
-
id = @last_macro
|
|
68
|
-
end
|
|
109
|
+
id = @last_macro if id.nil?
|
|
69
110
|
@named_macros[name] = @recorded_macros[id].clone
|
|
111
|
+
save_named_macros
|
|
112
|
+
message("Macro '#{name}' saved")
|
|
70
113
|
end
|
|
71
114
|
|
|
72
115
|
def start_recording(name)
|
|
@@ -122,6 +165,8 @@ class Macro
|
|
|
122
165
|
isok = true
|
|
123
166
|
# if acts.kind_of?(Array) and acts.any?
|
|
124
167
|
if acts.any?
|
|
168
|
+
vma.buf&.new_undo_group
|
|
169
|
+
vma.buf&.instance_variable_set(:@macro_group_active, true)
|
|
125
170
|
@running_macro = true
|
|
126
171
|
# TODO:needed?
|
|
127
172
|
# set_last_command({ method: vma.macro.method("run_macro"), params: [name] })
|
|
@@ -138,6 +183,8 @@ class Macro
|
|
|
138
183
|
end
|
|
139
184
|
end
|
|
140
185
|
@running_macro = false
|
|
186
|
+
vma.buf&.instance_variable_set(:@macro_group_active, false)
|
|
187
|
+
vma.buf&.new_undo_group
|
|
141
188
|
buf.set_pos(buf.pos)
|
|
142
189
|
# TODO: Should be a better way to trigger this. Sometimes need to wait for GTK to process things before updating the cursor.
|
|
143
190
|
run_as_idle proc { vma.buf.refresh_cursor; vma.buf.refresh_cursor }, delay: 0.15
|
data/lib/vimamsa/rbvma.rb
CHANGED
|
@@ -33,6 +33,7 @@ require "vimamsa/gui_menu"
|
|
|
33
33
|
require "vimamsa/gui_dialog"
|
|
34
34
|
require "vimamsa/gui_settings"
|
|
35
35
|
require "vimamsa/gui_file_panel"
|
|
36
|
+
require "vimamsa/gui_func_panel"
|
|
36
37
|
require "vimamsa/gui_select_window"
|
|
37
38
|
require "vimamsa/gui_sourceview"
|
|
38
39
|
require "vimamsa/gui_sourceview_autocomplete"
|
|
@@ -49,6 +50,7 @@ require "vimamsa/buffer_manager"
|
|
|
49
50
|
require "vimamsa/constants"
|
|
50
51
|
require "vimamsa/debug"
|
|
51
52
|
require "vimamsa/tests"
|
|
53
|
+
require "vimamsa/test_framework"
|
|
52
54
|
require "vimamsa/easy_jump"
|
|
53
55
|
require "vimamsa/encrypt"
|
|
54
56
|
require "vimamsa/file_finder"
|