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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +32 -0
  3. data/Dockerfile +45 -0
  4. data/custom_example.rb +37 -11
  5. data/docker_cmd.sh +7 -0
  6. data/exe/run_tests.rb +23 -0
  7. data/lib/vimamsa/actions.rb +8 -0
  8. data/lib/vimamsa/buffer.rb +38 -47
  9. data/lib/vimamsa/buffer_changetext.rb +49 -12
  10. data/lib/vimamsa/buffer_list.rb +2 -28
  11. data/lib/vimamsa/conf.rb +30 -0
  12. data/lib/vimamsa/diff_buffer.rb +80 -32
  13. data/lib/vimamsa/editor.rb +54 -67
  14. data/lib/vimamsa/file_finder.rb +6 -2
  15. data/lib/vimamsa/gui.rb +247 -63
  16. data/lib/vimamsa/gui_file_panel.rb +1 -0
  17. data/lib/vimamsa/gui_func_panel.rb +127 -0
  18. data/lib/vimamsa/gui_menu.rb +42 -0
  19. data/lib/vimamsa/gui_select_window.rb +17 -6
  20. data/lib/vimamsa/gui_settings.rb +347 -13
  21. data/lib/vimamsa/gui_sourceview.rb +116 -2
  22. data/lib/vimamsa/gui_text.rb +0 -22
  23. data/lib/vimamsa/hyper_plain_text.rb +1 -0
  24. data/lib/vimamsa/key_actions.rb +30 -29
  25. data/lib/vimamsa/key_binding_tree.rb +85 -3
  26. data/lib/vimamsa/key_bindings_vimlike.rb +4 -0
  27. data/lib/vimamsa/langservp.rb +161 -7
  28. data/lib/vimamsa/macro.rb +54 -7
  29. data/lib/vimamsa/rbvma.rb +2 -0
  30. data/lib/vimamsa/test_framework.rb +137 -0
  31. data/lib/vimamsa/version.rb +1 -1
  32. data/modules/calculator/calculator.rb +318 -0
  33. data/modules/calculator/calculator_info.rb +3 -0
  34. data/modules/terminal/terminal.rb +140 -0
  35. data/modules/terminal/terminal_info.rb +3 -0
  36. data/run_tests.rb +89 -0
  37. data/styles/dark.xml +1 -1
  38. data/styles/molokai_edit.xml +2 -2
  39. data/tests/key_bindings.rb +2 -0
  40. data/tests/test_basic_editing.rb +86 -0
  41. data/tests/test_copy_paste.rb +88 -0
  42. data/tests/test_key_bindings.rb +152 -0
  43. data/tests/test_module_interface.rb +98 -0
  44. data/tests/test_undo.rb +201 -0
  45. data/vimamsa.gemspec +6 -5
  46. metadata +46 -14
@@ -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(BACKWARD_CHAR) }, "Select text downwards")
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(:ack_search, proc { gui_ack }, "Ack")
119
-
120
- reg_act(:copy_cur_line, proc {buf.copy_line}, "Copy the current line")
121
- reg_act(:paste_before_cursor, proc {buf.paste(BEFORE)}, "Paste text before the cursor")
122
- reg_act(:paste_after_cursor, proc {buf.paste(AFTER)}, "Paste text after the cursor")
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
- :desc => "Increment word", :group => [:edit, :extra] },
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("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
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",
@@ -90,7 +90,7 @@ class LangSrv
90
90
  end
91
91
 
92
92
  def handle_delta(delta, fpath, version)
93
- fpuri = URI.join("file:///", fpath).to_s
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(fpuri, lpos, cpos)
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
- def open_file(fp, fc = nil)
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: "c++",
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 = vma.marshal_load("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"