vimamsa 0.1.22 → 0.1.24

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +32 -0
  3. data/Dockerfile +45 -0
  4. data/README.md +2 -2
  5. data/custom_example.rb +38 -9
  6. data/docker_cmd.sh +7 -0
  7. data/exe/run_tests.rb +23 -0
  8. data/img/screenshot1.png +0 -0
  9. data/img/screenshot2.png +0 -0
  10. data/lib/vimamsa/actions.rb +8 -0
  11. data/lib/vimamsa/buffer.rb +165 -53
  12. data/lib/vimamsa/buffer_changetext.rb +68 -14
  13. data/lib/vimamsa/buffer_cursor.rb +9 -3
  14. data/lib/vimamsa/buffer_list.rb +14 -28
  15. data/lib/vimamsa/buffer_manager.rb +1 -1
  16. data/lib/vimamsa/conf.rb +33 -1
  17. data/lib/vimamsa/diff_buffer.rb +185 -0
  18. data/lib/vimamsa/editor.rb +149 -80
  19. data/lib/vimamsa/file_finder.rb +6 -2
  20. data/lib/vimamsa/gui.rb +330 -135
  21. data/lib/vimamsa/gui_dialog.rb +2 -0
  22. data/lib/vimamsa/gui_file_panel.rb +94 -0
  23. data/lib/vimamsa/gui_form_generator.rb +4 -2
  24. data/lib/vimamsa/gui_func_panel.rb +127 -0
  25. data/lib/vimamsa/gui_image.rb +2 -4
  26. data/lib/vimamsa/gui_menu.rb +54 -1
  27. data/lib/vimamsa/gui_select_window.rb +18 -6
  28. data/lib/vimamsa/gui_settings.rb +486 -0
  29. data/lib/vimamsa/gui_sourceview.rb +196 -8
  30. data/lib/vimamsa/gui_text.rb +0 -22
  31. data/lib/vimamsa/hyper_plain_text.rb +1 -0
  32. data/lib/vimamsa/key_actions.rb +54 -31
  33. data/lib/vimamsa/key_binding_tree.rb +154 -8
  34. data/lib/vimamsa/key_bindings_vimlike.rb +48 -35
  35. data/lib/vimamsa/langservp.rb +161 -7
  36. data/lib/vimamsa/macro.rb +54 -7
  37. data/lib/vimamsa/main.rb +1 -0
  38. data/lib/vimamsa/rbvma.rb +5 -0
  39. data/lib/vimamsa/string_util.rb +56 -0
  40. data/lib/vimamsa/test_framework.rb +137 -0
  41. data/lib/vimamsa/util.rb +3 -36
  42. data/lib/vimamsa/version.rb +1 -1
  43. data/modules/calculator/calculator.rb +318 -0
  44. data/modules/calculator/calculator_info.rb +3 -0
  45. data/modules/terminal/terminal.rb +140 -0
  46. data/modules/terminal/terminal_info.rb +3 -0
  47. data/run_tests.rb +89 -0
  48. data/styles/dark.xml +1 -1
  49. data/styles/molokai_edit.xml +2 -2
  50. data/tests/key_bindings.rb +2 -0
  51. data/tests/test_basic_editing.rb +86 -0
  52. data/tests/test_copy_paste.rb +88 -0
  53. data/tests/test_key_bindings.rb +152 -0
  54. data/tests/test_module_interface.rb +98 -0
  55. data/tests/test_undo.rb +201 -0
  56. data/vimamsa.gemspec +6 -5
  57. metadata +52 -14
@@ -19,7 +19,7 @@
19
19
  #
20
20
 
21
21
  class State
22
- attr_accessor :key_name, :eval_rule, :children, :action, :label, :major_modes, :level, :cursor_type, :keywords
22
+ attr_accessor :key_name, :eval_rule, :children, :action, :label, :major_modes, :level, :cursor_type, :keywords, :root, :parent
23
23
  attr_reader :cur_mode, :scope
24
24
 
25
25
  def initialize(key_name, eval_rule = "", ctype = :command, scope: :buffer)
@@ -31,6 +31,8 @@ class State
31
31
  @keywords = []
32
32
  @action = nil
33
33
  @level = 0
34
+ @root = self # parent of a parent ... until mode root
35
+ @parent = nil
34
36
  @cursor_type = ctype
35
37
  end
36
38
 
@@ -55,6 +57,7 @@ class KeyBindingTree
55
57
  @last_action = nil
56
58
  @cur_action = nil
57
59
  @method_handles_repeat = false
60
+ @overwriting_state = nil # A branch which has priority over other branches
58
61
 
59
62
  @modifiers = { :ctrl => false, :shift => false, :alt => false } # TODO: create a queue
60
63
  @last_event = [nil, nil, nil, nil, nil]
@@ -66,10 +69,12 @@ class KeyBindingTree
66
69
 
67
70
  def set_mode(label)
68
71
  return if get_mode == :label
72
+ vma.buf&.new_undo_group
69
73
  @match_state = [@modes[label]] # used for matching input
70
74
  @mode_root_state = @modes[label]
71
- # @default_mode = label
72
- @default_mode_stack << label
75
+
76
+ #TODO: should not happen? @default_mode_stack[-1] should be always the same as get_mode ?
77
+ @default_mode_stack << label if label != @default_mode_stack[-1]
73
78
 
74
79
  __set_mode(label)
75
80
  if !vma.buf.nil?
@@ -258,17 +263,78 @@ class KeyBindingTree
258
263
  end
259
264
 
260
265
  def set_state_to_root
266
+ # if root state is a minor mode
261
267
  if @mode_root_state.major_modes.size == 1
262
- modelabel = @mode_root_state.major_modes[0]
263
- mmode = @modes[modelabel]
264
- @match_state = [@mode_root_state, mmode]
268
+ modelabel = @mode_root_state.major_modes[0] #TODO: support multiple inheritance?
269
+ parent_mode = @modes[modelabel]
270
+ # Have two branches for the matching (both major and minor modes)
271
+ # @mode_root_state = minor, parent_mode = major
272
+ @match_state = [@mode_root_state, parent_mode]
273
+ @overwriting_state = @mode_root_state # States from this branch have priority over others.
265
274
  else
275
+ # if root state is a major mode
276
+ @overwriting_state = nil
266
277
  @match_state = [@mode_root_state]
267
278
  end
268
279
 
269
280
  @state_trail = [@mode_root_state]
270
281
  end
271
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
+
272
338
  def get_by_keywords(modes: [], keywords: [])
273
339
  s = ""
274
340
  stack = [[@root, ""]]
@@ -378,7 +444,13 @@ class KeyBindingTree
378
444
  mmid = st.major_modes.first
379
445
  trailpfx = "#{@modes[mmid].to_s}>"
380
446
  end
381
- s_trail << "[#{trailpfx}#{st.to_s}]"
447
+ mode_str = st.to_s
448
+ mode_str = "COMMAND" if mode_str == "C"
449
+ mode_str = "INSERT" if mode_str == "I"
450
+ mode_str = "VISUAL" if mode_str == "V"
451
+ mode_str = "BROWSE" if mode_str == "B"
452
+
453
+ s_trail << "[#{trailpfx}#{mode_str}]"
382
454
  else
383
455
  s_trail << " #{st.to_s}"
384
456
  end
@@ -491,7 +563,31 @@ class KeyBindingTree
491
563
  state_with_children = new_state.select { |s| s.children.any? }
492
564
  s_act = new_state.select { |s| s.action != nil }
493
565
 
494
- if s_act.any? #and !state_with_children.any?
566
+ # Multiple matching states/modes (search has forked)
567
+ if new_state.size > 1
568
+ # puts "AAA"
569
+ # Conflict: One of them has actions (matching should stop),
570
+ # another has children (matching should continue)
571
+ if s_act.any? and state_with_children.any?
572
+ # puts "AAA1"
573
+ a = s_act[0]
574
+ b = state_with_children[0]
575
+ # Running major+minor mode. Minor mode overwriting the major mode
576
+ if a.root == @overwriting_state or b.root == @overwriting_state
577
+ # puts "AAA3:del"
578
+ # Remove those states not belonging to the overwriting branch (minor mode)
579
+ [s_act, state_with_children, new_state].each { |z| z.delete_if { |x| x.root != @overwriting_state } }
580
+ end
581
+ end
582
+ end
583
+ # new_state[0].root.key_name
584
+
585
+ if s_act.any? and state_with_children.any?
586
+ # debug "Conflict: s_act.any? and state_with_children.any?"
587
+ # require "pry"; binding.pry
588
+ end
589
+
590
+ if s_act.any? and !state_with_children.any?
495
591
  eval_s = s_act.first.action if eval_s == nil
496
592
  # if eval_s.to_s.match(/end_recording/)
497
593
  # require "pry"; binding.pry
@@ -537,6 +633,14 @@ class KeyBindingTree
537
633
  key.each { |k| _bindkey(k, a, keywords: keywords) }
538
634
  end
539
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
+
540
644
  def _bindkey(key, action, keywords: [])
541
645
  key.strip!
542
646
  key.gsub!(/\s+/, " ")
@@ -625,6 +729,8 @@ class KeyBindingTree
625
729
  end
626
730
  s1 = new_state
627
731
  @cur_state.children << new_state
732
+ new_state.root = @cur_state.root
733
+ new_state.parent = @cur_state
628
734
  end
629
735
 
630
736
  set_state(key_name, eval_rule) # TODO: check is ok?
@@ -639,6 +745,7 @@ class KeyBindingTree
639
745
  end
640
746
 
641
747
  def handle_key_bindigs_action(action, c)
748
+ trail_str = get_state_trail_str[0]
642
749
  # $acth << action #TODO:needed here?
643
750
  @method_handles_repeat = false #TODO:??
644
751
  n = 1
@@ -685,6 +792,20 @@ class KeyBindingTree
685
792
  if !(action.class == String and action.include?("set_next_command_count"))
686
793
  @next_command_count = nil
687
794
  end
795
+
796
+ if cnf.kbd.show_prev_action? and trail_str.class == String
797
+ len_limit = 35
798
+ action_desc = "UNK"
799
+ if action.class == String && (m = action.match(/\Abuf\.insert_txt\((.+)\)\z/))
800
+ char_part = m[1].gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
801
+ action_desc = "insert #{char_part}"
802
+ else
803
+ action_desc = vma.actions[action]&.method_name || action.to_s
804
+ action_desc = action_desc[0..len_limit] if action_desc.size > len_limit
805
+ end
806
+ trail_str = trail_str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
807
+ vma.gui.action_trail_label.markup = "<span weight='bold'>#{action_desc}|#{trail_str}</span>"
808
+ end
688
809
  end
689
810
  end
690
811
 
@@ -692,6 +813,10 @@ def bindkey(key, action, keywords: "")
692
813
  vma.kbd.bindkey(key, action, keywords: keywords)
693
814
  end
694
815
 
816
+ def unbindkey(key)
817
+ vma.kbd.unbindkey(key)
818
+ end
819
+
695
820
  def add_keys(keywords, to_add)
696
821
  to_add.each { |key, value|
697
822
  bindkey(key, value, keywords: keywords)
@@ -705,11 +830,26 @@ def exec_action(action)
705
830
  return call_action(action)
706
831
  elsif action.class == Proc
707
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)
708
836
  else
709
837
  return eval(action)
710
838
  end
711
839
  end
712
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
+
713
853
  def show_key_bindings()
714
854
  kbd_s = "❙Key bindings❙\n"
715
855
  kbd_s << "\n⦁[Mode] <keys> : <action>⦁\n"
@@ -734,6 +874,12 @@ def show_key_bindings()
734
874
  done.concat(x.lines); kbd_s << x
735
875
  kbd_s << "\n"
736
876
 
877
+ kbd_s << "◼ Hyper Plaintext\n"
878
+ x = vma.kbd.get_by_keywords(modes: ["C"], keywords: ["hyperplaintext"])
879
+ x2 = vma.kbd.get_by_keywords(modes: ["V"], keywords: ["hyperplaintext"])
880
+ done.concat(x.lines); kbd_s << x << "\n" << x2
881
+ kbd_s << "\n"
882
+
737
883
  kbd_s << "◼ Core\n"
738
884
  x = vma.kbd.get_by_keywords(modes: [], keywords: ["core"])
739
885
  x << vma.kbd.get_by_keywords(modes: ["X"], keywords: ["intro"])
@@ -42,6 +42,14 @@ def insert_move(op)
42
42
  end
43
43
 
44
44
  add_keys "intro", {
45
+ "VCX up" => "buf.move(BACKWARD_LINE)",
46
+ "VCX down" => "buf.move(FORWARD_LINE)",
47
+ "VCX right" => "buf.move(FORWARD_CHAR)",
48
+ "VCX left" => "buf.move(BACKWARD_CHAR)",
49
+ "C , b" => :start_buf_manager,
50
+ "VC l" => "buf.move(FORWARD_CHAR)",
51
+ "VC , , s" => :search_actions,
52
+ "C , n b" => :buf_new,
45
53
  "C y y" => :copy_cur_line,
46
54
  "C P" => :paste_before_cursor,
47
55
  "C p" => :paste_after_cursor,
@@ -70,6 +78,20 @@ add_keys "intro", {
70
78
  "IX ctrl-n" => :move_next_line,
71
79
  "IX ctrl-b" => :move_backward_char,
72
80
  "IX ctrl-a" => :jump_beginning_of_line,
81
+
82
+ "CI ctrl-v" => :paste_before_cursor,
83
+ "I ctrl-c" => :copy_selection, #TODO: in control mode also?
84
+ "CI ctrl-x" => :cut_selection,
85
+ "CI ctrl-z" => :undo,
86
+ "CI ctrl-w" => :close_current_buffer,
87
+ "I ctrl-y" => :redo,
88
+ "CI backspace" => :insert_backspace,
89
+ "CI ctrl-o" => :open_file_dialog,
90
+ "I ctrl-l" => :find_in_buffer,
91
+ "I ctrl-g" => :find_next,
92
+
93
+ "I shift-up" => :insert_select_up,
94
+ "I shift-down" => :insert_select_down,
73
95
  }
74
96
 
75
97
  add_keys "intro delete", {
@@ -83,21 +105,16 @@ add_keys "core", {
83
105
  # File handling
84
106
  "C ctrl-s" => :buf_save,
85
107
 
108
+ "V J" => :V_join_lines,
86
109
  # Buffer handling
87
110
  # "C B" => "bufs.switch",
88
111
  "C tab" => "bufs.switch_to_last_buf",
89
112
  # 'C , s'=> 'gui_select_buffer',
90
113
  "C , r v b" => :buf_revert,
91
114
  "C , c b" => "bufs.close_current_buffer",
92
- "C , n b" => :buf_new,
93
115
  # "C , , ." => "backup_all_buffers()",
94
- "VC , , s" => :search_actions,
95
116
 
96
117
  # MOVING
97
- # 'VC h' => 'buf.move(BACKWARD_CHAR)',
98
- "VC l" => "buf.move(FORWARD_CHAR)",
99
- # "VC j" => "buf.move(FORWARD_LINE)",
100
- # "VC k" => "buf.move(BACKWARD_LINE)",
101
118
 
102
119
  "VCI pagedown" => :page_down,
103
120
  "VCI pageup" => :page_up,
@@ -113,10 +130,10 @@ add_keys "core", {
113
130
 
114
131
  "I enter" => :insert_new_line,
115
132
 
116
- "I shift-down" => "insert_select_move(BACKWARD_CHAR)",
133
+
134
+ "I shift-left" => "insert_select_move(BACKWARD_CHAR)",
117
135
  "I shift-right" => "insert_select_move(FORWARD_CHAR)",
118
136
  "I shift-down" => "insert_select_move(FORWARD_LINE)",
119
- "I shift-up" => "insert_select_move(BACKWARD_LINE)",
120
137
  "I shift-pagedown" => "insert_select_move(:pagedown)",
121
138
  "I shift-pageup" => "insert_select_move(:pageup)",
122
139
 
@@ -127,14 +144,6 @@ add_keys "core", {
127
144
  "I pagedown" => "insert_move(:pagedown)",
128
145
  "I pageup" => "insert_move(:pageup)",
129
146
 
130
- #TODO:
131
- "I @shift-click" => "insert_mode_shift_click(charpos)",
132
-
133
- "VCX left" => "buf.move(BACKWARD_CHAR)",
134
- "VCX right" => "buf.move(FORWARD_CHAR)",
135
- "VCX down" => "buf.move(FORWARD_LINE)",
136
- "VCX up" => "buf.move(BACKWARD_LINE)",
137
-
138
147
  # 'C '=> 'buf.jump_word(BACKWARD,END)',#TODO
139
148
  "VC f <char>" => "buf.jump_to_next_instance_of_char(<char>)",
140
149
  "VC F <char>" => "buf.jump_to_next_instance_of_char(<char>,BACKWARD)",
@@ -159,7 +168,7 @@ add_keys "core", {
159
168
 
160
169
  "R <char>" => "readchar_new_char(<char>)",
161
170
 
162
- "C n" => "$search.jump_to_next()",
171
+ "C n" => :find_next,
163
172
  "C N" => "$search.jump_to_previous()",
164
173
 
165
174
  "C C" => :content_search,
@@ -224,8 +233,6 @@ add_keys "core", {
224
233
  # "V ctrl-c" => "buf.comment_selection",
225
234
  "V ctrl-x" => "buf.comment_selection(:uncomment)",
226
235
 
227
- "CI ctrl-v" => "buf.paste(BEFORE)",
228
- "CI backspace" => "buf.delete(BACKWARD_CHAR)",
229
236
 
230
237
  # Marks
231
238
  "CV m <char>" => "buf.mark_current_position(<char>)",
@@ -237,6 +244,8 @@ add_keys "core", {
237
244
 
238
245
  # Replace mode
239
246
  "X esc || X ctrl!" => "vma.kbd.to_previous_mode",
247
+ # "X p" => :paste_over_after, #TODO
248
+ "X ctrl-v" => :paste_over_before,
240
249
  "X <char>" => "buf.replace_with_char(<char>);buf.move(FORWARD_CHAR)",
241
250
 
242
251
  # Macros
@@ -244,19 +253,22 @@ add_keys "core", {
244
253
  # "C q a" => 'vma.macro.start_recording("a")',
245
254
 
246
255
  "macro q" => "vma.kbd.to_previous_mode; vma.macro.end_recording",
247
- "macro q z" => "vma.kbd.to_previous_mode; vma.macro.end_recording",
256
+ # "macro q z" => "vma.kbd.to_previous_mode; vma.macro.end_recording",
248
257
 
249
258
  # "VC q(vma.macro.is_recording==true)" => "vma.macro.end_recording", # TODO: does not work
250
259
  # "VC o(vma.macro.is_recording==true)" => "vma.macro.end_recording", # TODO: does not work
251
260
  # "VC q q(vma.macro.is_recording==true)" => "vma.macro.end_recording",
252
261
  "VC q <char>" => "vma.kbd.set_mode(:macro);vma.macro.start_recording(<char>)",
253
262
  # 'C q'=> 'vma.macro.end_recording', #TODO
254
- "C q v" => "vma.kbd.to_previous_mode; vma.macro.end_recording",
263
+
264
+ # "C q v" => "vma.kbd.to_previous_mode; vma.macro.end_recording", #TODO
265
+
255
266
  # 'C v'=> 'vma.macro.end_recording',
256
267
  # "C M" => 'vma.macro.run_last_macro',
257
268
  "C @ <char>" => "vma.macro.run_macro(<char>)",
258
269
  "C , m S" => 'vma.macro.save_macro("a")',
259
270
  "C , m s" => "vma.macro.save",
271
+ "C , m h" => :show_message_history,
260
272
 
261
273
  # "C ." => "repeat_last_action", # TODO
262
274
  "VC ;" => "repeat_last_find",
@@ -277,7 +289,6 @@ add_keys "core", {
277
289
  "I space" => 'buf.insert_txt(" ")',
278
290
  # "I return" => 'buf.insert_new_line()',
279
291
 
280
- "CI ctrl-o" => :open_file_dialog,
281
292
  "C , a" => :ack_search,
282
293
  "C d w" => :delete_to_next_word_start,
283
294
 
@@ -305,19 +316,6 @@ add_keys "core", {
305
316
  "C , , u" => :update_file_index,
306
317
  "C , s a" => :buf_save_as,
307
318
  "VC , r r" => :gui_search_replace,
308
- "V , t b" => :set_style_bold,
309
- "V , t l" => :set_style_link,
310
- "V J" => :V_join_lines,
311
- "V , t c" => :clear_formats,
312
- "C , t h" => :set_line_style_heading,
313
- "C , t 1" => :set_line_style_h1,
314
- "C , t 2" => :set_line_style_h2,
315
- "C , t 3" => :set_line_style_h3,
316
- "C , t 4" => :set_line_style_h4,
317
- "C , t b" => :set_line_style_bold,
318
- "C , t t" => :set_line_style_title,
319
- "C , t c" => :clear_line_styles,
320
- "C , b" => :start_buf_manager,
321
319
  "C , w" => :toggle_active_window,
322
320
  "C , , w" => :toggle_two_column,
323
321
 
@@ -341,6 +339,20 @@ add_keys "core", {
341
339
  "V a d" => [:delete_append_selection, proc { buf.delete(SELECTION, :append) }, "Delete and append selection"]
342
340
  }
343
341
 
342
+ add_keys "hyperplaintext", {
343
+ "V , t b" => :set_style_bold,
344
+ "V , t l" => :set_style_link,
345
+ "V , t c" => :clear_formats,
346
+ "C , t h" => :set_line_style_heading,
347
+ "C , t 1" => :set_line_style_h1,
348
+ "C , t 2" => :set_line_style_h2,
349
+ "C , t 3" => :set_line_style_h3,
350
+ "C , t 4" => :set_line_style_h4,
351
+ "C , t b" => :set_line_style_bold,
352
+ "C , t t" => :set_line_style_title,
353
+ "C , t c" => :clear_line_styles,
354
+ }
355
+
344
356
  bindkey ["VCB M", "B m"], :run_last_macro
345
357
 
346
358
  add_keys "experimental", {
@@ -352,6 +364,7 @@ add_keys "experimental", {
352
364
  # "CV , R" => "restart_application", #TODO: does not work
353
365
  "I ctrl-h" => :show_autocomplete, #TODO: does not work
354
366
  "C , d m" => :kbd_dump_state,
367
+ "C , ; ." => :increment_word,
355
368
  "C , d d" => "debug_dump_deltas",
356
369
  "C , d c" => "debug_dump_clipboard",
357
370
  "C , d b" => "debug_print_buffer",
@@ -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