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
data/lib/vimamsa/gui.rb CHANGED
@@ -126,12 +126,11 @@ def gui_create_buffer(id, bufo)
126
126
 
127
127
  view.register_signals()
128
128
 
129
- ssm = GtkSource::StyleSchemeManager.new
130
- ssm.set_search_path(ssm.search_path << ppath("styles/"))
131
- sty = ssm.get_scheme("molokai_edit")
129
+ sty = load_vimamsa_scheme
132
130
 
133
131
  buf1.highlight_matching_brackets = true
134
132
  buf1.style_scheme = sty
133
+ gui_apply_color_mode(sty) if sty
135
134
 
136
135
  view.set_highlight_current_line(true)
137
136
  view.set_show_line_numbers(true)
@@ -165,9 +164,6 @@ def gui_set_file_lang(id, lname)
165
164
  view.buffer.highlight_syntax = true
166
165
  end
167
166
 
168
- def gui_add_image(imgpath, pos)
169
- end
170
-
171
167
  # TODO:?
172
168
  def gui_select_window_close(arg = nil)
173
169
  end
@@ -198,7 +194,7 @@ end
198
194
 
199
195
  class VMAgui
200
196
  attr_accessor :buffers, :sw1, :sw2, :view, :buf1, :window, :delex, :statnfo, :overlay, :sws, :two_c
201
- attr_reader :two_column, :windows, :subtitle, :app, :active_window, :action_trail_label, :file_panel
197
+ attr_reader :two_column, :windows, :subtitle, :app, :active_window, :action_trail_label, :file_panel, :func_panel
202
198
 
203
199
  def initialize()
204
200
  @two_column = false
@@ -212,6 +208,13 @@ class VMAgui
212
208
  @img_resizer_active = false
213
209
  @windows = {}
214
210
  @app = nil
211
+ @entry_has_focus = false
212
+ end
213
+
214
+ # Called by embedded widgets (e.g. calculator entries) when a text entry
215
+ # gains or loses focus, so the vimamsa key handler knows to step aside.
216
+ def notify_entry_focus(active)
217
+ @entry_has_focus = active
215
218
  end
216
219
 
217
220
  def run
@@ -320,39 +323,131 @@ class VMAgui
320
323
  end
321
324
 
322
325
  def add_to_minibuf(msg)
323
- # return #TODO:gtk4
324
- startiter = @minibuf.buffer.get_iter_at(:offset => 0)
325
- @minibuf.buffer.insert(startiter, "#{msg}\n")
326
- @minibuf.signal_emit("move-cursor", Gtk::MovementStep.new(:PAGES), -1, false)
326
+ @minibuf_messages ||= []
327
+ @minibuf_messages.unshift(msg)
328
+ @minibuf_messages = @minibuf_messages.first(50)
329
+
330
+ @minibuf_label.label = msg
331
+ @minibuf_textview.buffer.text = @minibuf_messages.join("\n")
332
+
333
+ return if @minibuf_expanded
334
+
335
+ @minibuf_stack.visible_child_name = "label"
336
+ @minibuf_content.visible = true
337
+
338
+ @minibuf_vpane.position = @minibuf_vpane.height - 26
339
+
340
+ GLib::Source.remove(@minibuf_hide_source) if @minibuf_hide_source
341
+ @minibuf_hide_source = GLib::Timeout.add(7000) do
342
+ @minibuf_content.visible = false
343
+ @minibuf_hide_source = nil
344
+ false
345
+ end
327
346
  end
328
347
 
329
- def init_minibuffer()
330
- # Init minibuffer
331
- sw = Gtk::ScrolledWindow.new
332
- sw.set_policy(:automatic, :automatic)
333
- overlay = Gtk::Overlay.new
334
- overlay.set_child(sw)
335
- @vbox.attach(overlay, 0, 3, 2, 1)
336
- sw.set_size_request(-1, 12)
337
-
338
- view = VSourceView.new(nil, nil)
339
- view.set_highlight_current_line(false)
340
- view.set_show_line_numbers(false)
341
- # view.set_buffer(buf1)
342
- ssm = GtkSource::StyleSchemeManager.new
343
- ssm.set_search_path(ssm.search_path << ppath("styles/"))
344
- sty = ssm.get_scheme("molokai_edit")
345
- view.buffer.highlight_matching_brackets = false #TODO
346
- view.buffer.style_scheme = sty
348
+ def minibuf_toggle_expanded
349
+ @minibuf_expanded = !@minibuf_expanded
350
+ if @minibuf_expanded
351
+ GLib::Source.remove(@minibuf_hide_source) if @minibuf_hide_source
352
+ @minibuf_hide_source = nil
353
+ @minibuf_content.visible = true
354
+ @minibuf_stack.visible_child_name = "history"
355
+ run_as_idle proc {
356
+ pos = @minibuf_vpane.max_position - (@minibuf_history_height || 120)
357
+ @minibuf_vpane.position = [pos, 0].max
358
+ }
359
+ else
360
+ # Save height before collapsing so next open restores it
361
+ @minibuf_history_height = @minibuf_vpane.max_position - @minibuf_vpane.position
362
+ @minibuf_stack.visible_child_name = "label"
363
+ run_as_idle proc {
364
+ #TODO: automatic way of resizing doesn't work:
365
+ # h = [@minibuf_label.height, 24].max + 2
366
+ # @minibuf_vpane.position = @minibuf_vpane.max_position - h
367
+ @minibuf_vpane.position = @minibuf_vpane.height - 26
368
+ }
369
+ @minibuf_hide_source = GLib::Timeout.add(7000) do
370
+ @minibuf_content.visible = false
371
+ @minibuf_expanded = false
372
+ @minibuf_stack.visible_child_name = "label"
373
+ @minibuf_hide_source = nil
374
+ false
375
+ end
376
+ end
377
+ end
378
+
379
+ def show_message_history
380
+ minibuf_toggle_expanded
381
+ end
382
+
383
+ # ── editor-area helpers (delegates to @minibuf_vpane start child) ─────────
384
+
385
+ def editor_area
386
+ @minibuf_vpane.start_child
387
+ end
388
+
389
+ def set_editor_area(w)
390
+ @minibuf_vpane.set_start_child(w)
391
+ end
392
+
393
+ def init_minibuffer
394
+ @minibuf_messages = []
395
+ @minibuf_expanded = false
396
+ @minibuf_history_height = 120
397
+
398
+ css = "label.minibuf, textview.minibuf { color: #cdd6f4; font-family: Monospace; font-size: 10pt; padding: 3px 8px; background-color: #1e1e2e; }"
347
399
  provider = Gtk::CssProvider.new
348
- # provider.load(data: "textview { font-family: Monospace; font-size: 11pt; }")
349
- provider.load(data: "textview { font-family: Arial; font-size: 10pt; color:#eeeeee}")
350
- view.style_context.add_provider(provider)
351
- view.wrap_mode = :char
352
- @minibuf = view
353
- # startiter = view.buffer.get_iter_at(:offset => 0)
354
- message("STARTUP")
355
- sw.set_child(view)
400
+ provider.load(data: css)
401
+
402
+ # Collapsed view: single line, latest message
403
+ @minibuf_label = Gtk::Label.new("")
404
+ @minibuf_label.xalign = 0.0
405
+ @minibuf_label.hexpand = true
406
+ @minibuf_label.ellipsize = Pango::EllipsizeMode::END
407
+ @minibuf_label.add_css_class("minibuf")
408
+ @minibuf_label.style_context.add_provider(provider)
409
+
410
+ # Expanded view: scrollable history (fills whatever height the pane gives)
411
+ @minibuf_textview = Gtk::TextView.new
412
+ @minibuf_textview.editable = false
413
+ @minibuf_textview.cursor_visible = false
414
+ @minibuf_textview.wrap_mode = :word_char
415
+ @minibuf_textview.add_css_class("minibuf")
416
+ @minibuf_textview.style_context.add_provider(provider)
417
+ @minibuf_textview.vexpand = true
418
+
419
+ scroll = Gtk::ScrolledWindow.new
420
+ scroll.set_policy(:never, :automatic)
421
+ scroll.set_child(@minibuf_textview)
422
+ scroll.vexpand = true
423
+
424
+ @minibuf_stack = Gtk::Stack.new
425
+ @minibuf_stack.vhomogeneous = false
426
+ @minibuf_stack.transition_type = :crossfade
427
+ @minibuf_stack.transition_duration = 100
428
+ @minibuf_stack.add_named(@minibuf_label, "label")
429
+ @minibuf_stack.add_named(scroll, "history")
430
+
431
+ @minibuf_content = Gtk::Box.new(:vertical, 0)
432
+ @minibuf_content.append(Gtk::Separator.new(:horizontal))
433
+ @minibuf_content.append(@minibuf_stack)
434
+ @minibuf_content.visible = false # hidden until first message
435
+
436
+ # Vertical pane: editor fills top, minibuffer sits at bottom, draggable
437
+ editor_w = @windows[1][:overlay]
438
+ @vbox.remove(editor_w)
439
+
440
+ @minibuf_vpane = Gtk::Paned.new(:vertical)
441
+ @minibuf_vpane.vexpand = true
442
+ @minibuf_vpane.hexpand = true
443
+ @minibuf_vpane.resize_start_child = true # editor absorbs window resize
444
+ @minibuf_vpane.resize_end_child = false # minibuffer keeps its height
445
+ @minibuf_vpane.shrink_end_child = true # allow fully collapsing minibuffer
446
+ @minibuf_vpane.set_start_child(editor_w)
447
+ @minibuf_vpane.set_end_child(@minibuf_content)
448
+
449
+ @vbox.attach(@minibuf_vpane, 0, 2, 2, 1)
450
+ @minibuf_hide_source = nil
356
451
  end
357
452
 
358
453
  def make_header_button(action_id, icon, cb)
@@ -373,16 +468,17 @@ class VMAgui
373
468
  file_box.style_context.add_class("linked")
374
469
  file_box.append(make_header_button("hdr-open", "document-open-symbolic", proc { open_file_dialog }))
375
470
  file_box.append(make_header_button("hdr-save", "document-save-symbolic", proc { buf.save }))
376
- file_box.append(make_header_button("hdr-new", "document-new-symbolic", proc { create_new_file }))
471
+ file_box.append(make_header_button("hdr-new", "document-new-symbolic", proc { create_new_file }))
377
472
  header.pack_start(file_box)
378
473
 
379
474
  nav_box = Gtk::Box.new(:horizontal, 0)
380
475
  nav_box.style_context.add_class("linked")
381
- nav_box.append(make_header_button("hdr-prev", "pan-start-symbolic", proc { history_switch_backwards }))
382
- nav_box.append(make_header_button("hdr-next", "pan-end-symbolic", proc { history_switch_forwards }))
476
+ nav_box.append(make_header_button("hdr-prev", "pan-start-symbolic", proc { history_switch_backwards }))
477
+ nav_box.append(make_header_button("hdr-next", "pan-end-symbolic", proc { history_switch_forwards }))
478
+ nav_box.append(make_header_button("hdr-close", "window-close-symbolic", proc { bufs.close_current_buffer }))
383
479
  header.pack_start(nav_box)
384
480
 
385
- header.pack_end(make_header_button("hdr-close", "window-close-symbolic", proc { bufs.close_current_buffer }))
481
+ # header.pack_end()
386
482
 
387
483
  @window.titlebar = header
388
484
  end
@@ -429,6 +525,10 @@ class VMAgui
429
525
  @window.add_controller(press)
430
526
 
431
527
  press.signal_connect "key-pressed" do |gesture, keyval, keycode, y|
528
+ # Step aside when a text entry widget (e.g. calculator var field) has focus.
529
+ # @entry_has_focus is set explicitly via notify_entry_focus by those widgets.
530
+ next false if @entry_has_focus
531
+ next false if @kbd_passthrough
432
532
  name = Gdk::Keyval.to_name(keyval)
433
533
  uki = Gdk::Keyval.to_unicode(keyval)
434
534
  keystr = uki.chr("UTF-8")
@@ -457,6 +557,8 @@ class VMAgui
457
557
  end
458
558
 
459
559
  press.signal_connect "key-released" do |gesture, keyval, keycode, y|
560
+ next false if @entry_has_focus
561
+ next false if @kbd_passthrough
460
562
  name = Gdk::Keyval.to_name(keyval)
461
563
  uki = Gdk::Keyval.to_unicode(keyval)
462
564
  keystr = uki.chr("UTF-8")
@@ -509,8 +611,6 @@ class VMAgui
509
611
  @last_debug_idle = Time.now
510
612
  app = Gtk::Application.new("net.samiddhi.vimamsa.r#{rand(1000)}", :flags_none)
511
613
  @app = app
512
-
513
-
514
614
 
515
615
  Gtk::Settings.default.gtk_application_prefer_dark_theme = true
516
616
  Gtk::Settings.default.gtk_theme_name = "Adwaita"
@@ -521,6 +621,11 @@ class VMAgui
521
621
  @window = Gtk::ApplicationWindow.new(app)
522
622
  @window.set_application(app)
523
623
 
624
+ @window.signal_connect("close-request") do
625
+ vma.shutdown
626
+ true # prevent default destroy; shutdown->gui.quit handles it
627
+ end
628
+
524
629
  @window.title = "Multiple Views"
525
630
  @vpaned = Gtk::Paned.new(:vertical)
526
631
 
@@ -545,9 +650,9 @@ class VMAgui
545
650
  motion_controller.signal_connect("motion") do |controller, x, y|
546
651
  # label.set_text("Mouse at: (%.1f, %.1f)" % [x, y])
547
652
  # puts "MOVE #{x} #{y}"
548
-
653
+
549
654
  # Cursor vanishes when hovering over menubar
550
- draw_cursor_bug_workaround if y < 30
655
+ draw_cursor_bug_workaround if y < 30
551
656
  @last_cursor = [x, y]
552
657
  @cursor_move_time = Time.now
553
658
  end
@@ -588,10 +693,10 @@ class VMAgui
588
693
 
589
694
  # TODO: Doesn't work, why?:
590
695
  # menubar_bar = Gtk::PopoverMenuBar.new(menu_model: menubar)
591
-
696
+
592
697
  menubar_bar = Gtk::PopoverMenuBar.new()
593
698
  menubar_bar.set_menu_model(menubar)
594
-
699
+
595
700
  menubar_bar.hexpand = true
596
701
  @action_trail_label = Gtk::Label.new("")
597
702
  @action_trail_label.add_css_class("action-trail")
@@ -604,6 +709,7 @@ class VMAgui
604
709
 
605
710
  init_header_bar
606
711
  file_panel_init
712
+ func_panel_init
607
713
 
608
714
  @window.show
609
715
 
@@ -672,7 +778,11 @@ class VMAgui
672
778
  end
673
779
 
674
780
  def init_menu
675
- Vimamsa::Menu.new(@menubar, @app)
781
+ @menu = Vimamsa::Menu.new(@menubar, @app)
782
+ end
783
+
784
+ def menu
785
+ @menu
676
786
  end
677
787
 
678
788
  def toggle_two_column
@@ -696,9 +806,13 @@ class VMAgui
696
806
  @pane.set_start_child(nil)
697
807
  @pane.set_end_child(nil)
698
808
 
699
- @vbox.remove(@pane)
700
- @vbox.attach(w1[:overlay], 0, 2, 2, 1)
701
- # @vbox.attach(@statbox, 1, 1, 1, 1)
809
+ if @func_panel_shown
810
+ @func_panel_pane.set_end_child(w1[:overlay])
811
+ elsif @file_panel_shown
812
+ @file_panel_pane.set_end_child(w1[:overlay])
813
+ else
814
+ set_editor_area(w1[:overlay])
815
+ end
702
816
  @two_column = false
703
817
  end
704
818
 
@@ -748,14 +862,24 @@ class VMAgui
748
862
  w1 = @windows[1]
749
863
  w2 = @windows[2]
750
864
 
751
- # Remove overlay from @vbox and add the Gtk::Paned instead
865
+ # Remove overlay from its current parent and add the Gtk::Paned instead
752
866
  @pane = Gtk::Paned.new(:horizontal)
753
- @vbox.remove(w1[:overlay])
754
- @pane.set_start_child(w2[:overlay])
755
- @pane.set_end_child(w1[:overlay])
756
-
757
- # numbers: left, top, width, height
758
- @vbox.attach(@pane, 0, 2, 2, 1)
867
+ if @func_panel_shown
868
+ @func_panel_pane.set_end_child(nil)
869
+ @pane.set_start_child(w2[:overlay])
870
+ @pane.set_end_child(w1[:overlay])
871
+ @func_panel_pane.set_end_child(@pane)
872
+ elsif @file_panel_shown
873
+ @file_panel_pane.set_end_child(nil)
874
+ @pane.set_start_child(w2[:overlay])
875
+ @pane.set_end_child(w1[:overlay])
876
+ @file_panel_pane.set_end_child(@pane)
877
+ else
878
+ @minibuf_vpane.set_start_child(nil) # unparent w1[:overlay] before re-parenting
879
+ @pane.set_start_child(w2[:overlay])
880
+ @pane.set_end_child(w1[:overlay])
881
+ set_editor_area(@pane)
882
+ end
759
883
 
760
884
  w2[:sw].show
761
885
  @two_column = true
@@ -925,15 +1049,22 @@ class VMAgui
925
1049
 
926
1050
  def show_file_panel
927
1051
  return if @file_panel_shown
928
- inner = @two_column ? @pane : @windows[1][:overlay]
929
- @vbox.remove(inner)
1052
+ # If func panel is shown it sits in minibuf_vpane; wrap it too
1053
+ inner = if @func_panel_shown
1054
+ @func_panel_pane
1055
+ elsif @two_column
1056
+ @pane
1057
+ else
1058
+ @windows[1][:overlay]
1059
+ end
1060
+ @minibuf_vpane.set_start_child(nil) # unparent inner before re-parenting
930
1061
  @file_panel_pane = Gtk::Paned.new(:horizontal)
931
1062
  @file_panel_pane.hexpand = true
932
1063
  @file_panel_pane.vexpand = true
933
1064
  @file_panel_pane.set_start_child(@file_panel.widget)
934
1065
  @file_panel_pane.set_end_child(inner)
935
1066
  @file_panel_pane.set_position(180)
936
- @vbox.attach(@file_panel_pane, 0, 2, 2, 1)
1067
+ set_editor_area(@file_panel_pane)
937
1068
  @file_panel_shown = true
938
1069
  @file_panel.refresh
939
1070
  end
@@ -943,12 +1074,65 @@ class VMAgui
943
1074
  inner = @file_panel_pane.end_child
944
1075
  @file_panel_pane.set_start_child(nil)
945
1076
  @file_panel_pane.set_end_child(nil)
946
- @vbox.remove(@file_panel_pane)
947
- @vbox.attach(inner, 0, 2, 2, 1)
1077
+ set_editor_area(inner)
948
1078
  @file_panel_shown = false
949
1079
  end
950
1080
 
951
1081
  def toggle_file_panel
952
1082
  @file_panel_shown ? hide_file_panel : show_file_panel
953
1083
  end
1084
+
1085
+ def func_panel_init
1086
+ @func_panel = FuncPanel.new
1087
+ @func_panel_shown = false
1088
+ end
1089
+
1090
+ def func_panel_refresh
1091
+ return unless @func_panel_shown
1092
+ @func_panel.refresh
1093
+ end
1094
+
1095
+ def show_func_panel
1096
+ return if @func_panel_shown
1097
+ if @file_panel_shown
1098
+ inner = @file_panel_pane.end_child
1099
+ @file_panel_pane.set_end_child(nil)
1100
+ elsif @two_column
1101
+ inner = @pane
1102
+ @minibuf_vpane.set_start_child(nil)
1103
+ else
1104
+ inner = @windows[1][:overlay]
1105
+ @minibuf_vpane.set_start_child(nil)
1106
+ end
1107
+ @func_panel_pane = Gtk::Paned.new(:horizontal)
1108
+ @func_panel_pane.hexpand = true
1109
+ @func_panel_pane.vexpand = true
1110
+ @func_panel_pane.set_start_child(@func_panel.widget)
1111
+ @func_panel_pane.set_end_child(inner)
1112
+ @func_panel_pane.set_position(160)
1113
+ if @file_panel_shown
1114
+ @file_panel_pane.set_end_child(@func_panel_pane)
1115
+ else
1116
+ set_editor_area(@func_panel_pane)
1117
+ end
1118
+ @func_panel_shown = true
1119
+ @func_panel.refresh
1120
+ end
1121
+
1122
+ def hide_func_panel
1123
+ return unless @func_panel_shown
1124
+ inner = @func_panel_pane.end_child
1125
+ @func_panel_pane.set_start_child(nil)
1126
+ @func_panel_pane.set_end_child(nil)
1127
+ if @file_panel_shown
1128
+ @file_panel_pane.set_end_child(inner)
1129
+ else
1130
+ set_editor_area(inner)
1131
+ end
1132
+ @func_panel_shown = false
1133
+ end
1134
+
1135
+ def toggle_func_panel
1136
+ @func_panel_shown ? hide_func_panel : show_func_panel
1137
+ end
954
1138
  end
@@ -7,6 +7,7 @@ class FileTreePanel
7
7
  @tree = Gtk::TreeView.new(@store)
8
8
  @tree.headers_visible = false
9
9
  @tree.activate_on_single_click = true
10
+ @tree.level_indentation = 7
10
11
 
11
12
  renderer = Gtk::CellRendererText.new
12
13
  renderer.ellipsize = Pango::EllipsizeMode::START
@@ -0,0 +1,127 @@
1
+ # Left-side panel that displays LSP-provided functions/methods for the current buffer,
2
+ # grouped by the class or module they belong to.
3
+ class FuncPanel
4
+ COL_NAME = 0 # Tree column index: display name (class name or function name)
5
+ COL_LINE = 1 # Tree column index: 1-based line number to jump to; 0 = not a jump target
6
+
7
+ def initialize
8
+ # @store is the data model: a tree (not flat list) so we can nest functions
9
+ # under their parent class. Two columns: the display string and the line number.
10
+ @store = Gtk::TreeStore.new(String, Integer)
11
+
12
+ # @tree is the GTK widget that renders @store. It holds expand/collapse state
13
+ # and handles row selection. Backed by @store — clearing @store clears the view.
14
+ @tree = Gtk::TreeView.new(@store)
15
+ @tree.headers_visible = false
16
+ @tree.activate_on_single_click = true
17
+
18
+ # Single text column; ellipsize at end so long names don't overflow the panel width.
19
+ renderer = Gtk::CellRendererText.new
20
+ renderer.ellipsize = Pango::EllipsizeMode::END
21
+ col = Gtk::TreeViewColumn.new("", renderer, text: COL_NAME)
22
+ col.expand = true
23
+ @tree.append_column(col)
24
+
25
+ # Jump to the function's line when a row is clicked.
26
+ # Class header rows have COL_LINE = 0 and are skipped (no jump).
27
+ @tree.signal_connect("row-activated") do |_tv, path, _col|
28
+ iter = @store.get_iter(path)
29
+ next if iter.nil?
30
+ line = iter[COL_LINE]
31
+ next if line <= 0
32
+ vma.buf.jump_to_line(line)
33
+ end
34
+
35
+ sw = Gtk::ScrolledWindow.new
36
+ sw.set_policy(:never, :automatic)
37
+ sw.set_child(@tree)
38
+ sw.vexpand = true
39
+
40
+ header = Gtk::Label.new("<span weight='ultrabold'>Functions</span> (click to jump)")
41
+ header.use_markup = true
42
+
43
+ header.xalign = 0.0
44
+ header.margin_start = 6
45
+ header.margin_top = 4
46
+ header.margin_bottom = 2
47
+
48
+ # @box is the outermost widget: header label on top, scrollable tree below.
49
+ @box = Gtk::Box.new(:vertical, 0)
50
+ @box.set_size_request(160, -1)
51
+ @box.append(header)
52
+ @box.append(sw)
53
+ end
54
+
55
+ # Returns the outermost widget to embed in the paned layout.
56
+ def widget
57
+ @box
58
+ end
59
+
60
+ # Asynchronously fetch functions from the LSP server and repopulate @store.
61
+ # The LSP call runs in a background thread; the GTK update is marshalled back
62
+ # to the main thread via GLib::Idle.add to avoid threading issues.
63
+ def refresh
64
+ buf = vma.buf
65
+ unless buf&.fname
66
+ set_placeholder("(no file)")
67
+ return
68
+ end
69
+ lsp = LangSrv.get(buf.lang)
70
+ unless lsp
71
+ set_placeholder("(no LSP)")
72
+ return
73
+ end
74
+ fpath = buf.fname
75
+ Thread.new {
76
+ # groups: [{name:, line:, functions: [{name:, line:}, ...]}, ...]
77
+ # name: nil means top-level functions with no enclosing class.
78
+ groups = lsp.document_functions_grouped(fpath)
79
+ GLib::Idle.add {
80
+ @store.clear
81
+ if groups.nil? || groups.empty?
82
+ set_placeholder("(no functions)")
83
+ else
84
+ populate(groups)
85
+ end
86
+ false # returning false removes this idle callback after one run
87
+ }
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ # Fill @store from the grouped function list.
94
+ # Named groups become collapsible parent rows; top-level functions are root rows.
95
+ def populate(groups)
96
+ groups.each do |g|
97
+ if g[:name]
98
+ # Class/module header: parent row with the class name (and its own line number
99
+ # so clicking it jumps to the class definition when line > 0).
100
+ header = @store.append(nil)
101
+ header[COL_NAME] = g[:name]
102
+ header[COL_LINE] = g[:line] || 0
103
+ g[:functions].each do |f|
104
+ child = @store.append(header) # appended under the class header
105
+ child[COL_NAME] = f[:name]
106
+ child[COL_LINE] = f[:line]
107
+ end
108
+ else
109
+ # Top-level functions (not inside any class): added as root rows directly.
110
+ g[:functions].each do |f|
111
+ row = @store.append(nil)
112
+ row[COL_NAME] = f[:name]
113
+ row[COL_LINE] = f[:line]
114
+ end
115
+ end
116
+ end
117
+ @tree.expand_all # show all classes expanded by default
118
+ end
119
+
120
+ # Replace the entire store contents with a single non-clickable status message.
121
+ def set_placeholder(text)
122
+ @store.clear
123
+ iter = @store.append(nil)
124
+ iter[COL_NAME] = text
125
+ iter[COL_LINE] = 0
126
+ end
127
+ end
@@ -53,6 +53,7 @@ module Vimamsa
53
53
 
54
54
  add_to_menu "Actions.experimental.Diff", { :label => "Show Diff of\nunsaved changes", :action => :diff_buffer }
55
55
  add_to_menu "Actions.experimental.GitDiff", { :label => "Show git diff", :action => :git_diff_buffer }
56
+ add_to_menu "Actions.experimental.GitDiffW", { :label => "Show git diff -w (repo)", :action => :git_diff_w }
56
57
 
57
58
  add_to_menu "Actions.experimental.PrintBufferAccessList", { :label => "Print buffers by access time", :action => :print_buffer_access_list }
58
59
  add_to_menu "Actions.experimental.EnableDebug", { :label => "Enable debug", :action => :enable_debug }
@@ -63,10 +64,13 @@ module Vimamsa
63
64
 
64
65
 
65
66
  add_to_menu "Actions.debug.dumpkbd", { :label => "Dump kbd state", :action => :kbd_dump_state }
67
+ add_to_menu "Actions.debug.ToggleKbdPassthrough", { :label => "Toggle kbd event passthrough", :action => :toggle_kbd_passthrough }
66
68
 
67
69
  add_to_menu "View.BufferManager", { :label => "Show open files", :action => :start_buf_manager }
68
70
  add_to_menu "View.TwoColumn", { :label => "Toggle two column mode", :action => :toggle_two_column }
69
71
  add_to_menu "View.FilePanel", { :label => "Toggle file panel", :action => :toggle_file_panel }
72
+ add_to_menu "View.FuncPanel", { :label => "Toggle function panel (LSP)", :action => :toggle_func_panel }
73
+ add_to_menu "View.MsgHistory", { :label => "Message history", :action => :show_message_history }
70
74
 
71
75
  add_to_menu "Actions.EncryptFile", { :label => "Encrypt file", :action => :encrypt_file }
72
76
  add_to_menu "Help.KeyBindings", { :label => "Show key bindings", :action => :show_key_bindings }
@@ -81,6 +85,11 @@ module Vimamsa
81
85
  def initialize(menubar, _app)
82
86
  @app = _app
83
87
  @nfo = {}
88
+ @menubar = menubar
89
+ # Gio::Menu for the "Modules" top-level menu, created on first use.
90
+ @module_menu = nil
91
+ # Ordered list of action symbols in @module_menu, used to find removal positions.
92
+ @module_actions = []
84
93
 
85
94
  add_menu_items
86
95
 
@@ -89,6 +98,39 @@ module Vimamsa
89
98
  end
90
99
  end
91
100
 
101
+ # Add a menu item under the top-level "Modules" menu.
102
+ # Creates the Modules menu the first time it is called.
103
+ def add_module_action(action, label)
104
+ if @module_menu.nil?
105
+ @module_menu = Gio::Menu.new
106
+ modules_item = Gio::MenuItem.new("Modules", nil)
107
+ modules_item.submenu = @module_menu
108
+ @menubar.append_item(modules_item)
109
+ end
110
+
111
+ act = Gio::SimpleAction.new(action.to_s)
112
+ @app.add_action(act)
113
+ act.signal_connect("activate") { call_action(action) }
114
+
115
+ item = Gio::MenuItem.new(label, "app.#{action}")
116
+ @module_menu.append_item(item)
117
+ @module_actions << action
118
+ end
119
+
120
+ # Remove a previously added module menu item.
121
+ def remove_module_action(action)
122
+ idx = @module_actions.index(action)
123
+ return if idx.nil?
124
+ @module_menu.remove(idx)
125
+ @module_actions.delete_at(idx)
126
+ @app.remove_action(action.to_s)
127
+ end
128
+
129
+ # Return true if action was added via add_module_action and not yet removed.
130
+ def module_action?(action)
131
+ @module_actions.include?(action)
132
+ end
133
+
92
134
  def build_menu(nfo, parent)
93
135
  menu = Gio::Menu.new
94
136
  if nfo[:action]