vimamsa 0.1.23 → 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 (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 +344 -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
@@ -1,16 +1,13 @@
1
1
  # Map a "line number in a unified diff output" to the corresponding
2
- # line number in the *new/changed file* (the + side).
2
+ # line in the new/changed file (the + side), together with the file it belongs to.
3
3
  #
4
- # Key idea:
5
- # @@ -old_start,old_count +new_start,new_count @@
6
- # sets starting counters. Then walk each hunk line:
4
+ # Handles multi-file diffs: each --- / +++ pair sets the active file; each @@
5
+ # hunk header resets the line counters. Walking hunk lines:
7
6
  # ' ' => old++, new++
8
7
  # '-' => old++
9
8
  # '+' => new++
10
9
  #
11
- # If the target diff line is:
12
- # ' ' or '+' => it corresponds to current new line (before increment)
13
- # '-' => it has no new-file line (deleted). We return nil.
10
+ # Returns [new_path, old_path, line_no] or nil for deleted / unmappable lines.
14
11
  #
15
12
  class DiffLineMapper
16
13
  HUNK_RE = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/
@@ -20,18 +17,36 @@ class DiffLineMapper
20
17
  end
21
18
 
22
19
  # Given a 1-based line number in the diff output, return:
23
- # - Integer: 1-based line number in the new file
24
- # - nil: if the diff line is a deletion ('-') or cannot be mapped
20
+ # - [new_path, old_path, Integer]: raw +++ path, raw --- path, 1-based new-file line
21
+ # - nil: if the diff line is a deletion ('-') or cannot be mapped
25
22
  def new_line_for_diff_lineno(diff_lineno)
26
23
  raise ArgumentError, "diff line number must be >= 1" if diff_lineno.to_i < 1
27
24
  idx = diff_lineno.to_i - 1
28
25
  return nil if idx >= @lines.length
29
26
 
27
+ old_path = nil
28
+ new_path = nil
30
29
  old = nil
31
30
  new_ = nil
32
31
  in_hunk = false
33
32
 
34
33
  @lines.each_with_index do |line, i|
34
+ # File headers reset hunk state and record current file paths.
35
+ # These appear outside hunks, but guard against malformed diffs too.
36
+ if line.start_with?('--- ')
37
+ old_path = line[4..].split("\t").first.strip
38
+ in_hunk = false
39
+ old = new_ = nil
40
+ next
41
+ end
42
+
43
+ if line.start_with?('+++ ')
44
+ new_path = line[4..].split("\t").first.strip
45
+ in_hunk = false
46
+ old = new_ = nil
47
+ next
48
+ end
49
+
35
50
  if (m = line.match(HUNK_RE))
36
51
  old = m[1].to_i
37
52
  new_ = m[3].to_i
@@ -41,17 +56,11 @@ class DiffLineMapper
41
56
 
42
57
  next unless in_hunk
43
58
 
44
- if line.start_with?('--- ', '+++ ')
45
- in_hunk = false
46
- old = new_ = nil
47
- next
48
- end
49
-
50
59
  if i == idx
51
60
  return nil unless new_
52
61
  case line.getbyte(0)
53
- when '+'.ord then return new_
54
- when ' '.ord then return new_
62
+ when '+'.ord then return [new_path, old_path, new_]
63
+ when ' '.ord then return [new_path, old_path, new_]
55
64
  when '-'.ord then return nil
56
65
  else return nil
57
66
  end
@@ -94,23 +103,18 @@ end
94
103
  def diff_buffer_jump_to_source()
95
104
  mapper = DiffLineMapper.new(vma.buf.to_s)
96
105
  cur_lpos = vma.buf.lpos + 1
97
- to_line = mapper.new_line_for_diff_lineno(cur_lpos)
98
-
99
- orig_path = nil
100
- vma.buf.to_s.each_line do |l|
101
- if l =~ /^--- (.+)/
102
- path = $1.split("\t").first.strip
103
- # git diff prefixes paths with "a/" — strip it and resolve from git root
104
- if path.start_with?("a/")
105
- git_root = `git rev-parse --show-toplevel 2>/dev/null`.strip
106
- path = File.join(git_root, path[2..]) unless git_root.empty?
107
- end
108
- orig_path = path
109
- break
110
- end
106
+ result = mapper.new_line_for_diff_lineno(cur_lpos)
107
+
108
+ if result.nil?
109
+ message("No source line for this position")
110
+ return
111
111
  end
112
+
113
+ new_path, old_path, to_line = result
114
+ orig_path = resolve_diff_path(new_path, old_path)
115
+
112
116
  if orig_path.nil? || !File.exist?(orig_path)
113
- message("Could not find original file in diff")
117
+ message("Could not find file: #{new_path || old_path}")
114
118
  return
115
119
  end
116
120
 
@@ -118,6 +122,50 @@ def diff_buffer_jump_to_source()
118
122
  center_on_current_line
119
123
  end
120
124
 
125
+ # Resolve a +++ / --- path pair to an absolute filesystem path.
126
+ # Prefers new_path (the post-change file); falls back to old_path
127
+ # when new_path is /dev/null or a temp file that no longer exists.
128
+ def resolve_diff_path(new_path, old_path)
129
+ git_root = `git rev-parse --show-toplevel 2>/dev/null`.strip
130
+
131
+ expand = lambda do |path|
132
+ return nil if path.nil? || path == "/dev/null"
133
+ # git diff uses "a/" / "b/" prefixes for old/new sides
134
+ if path.start_with?("b/") || path.start_with?("a/")
135
+ rel = path[2..]
136
+ return git_root.empty? ? File.expand_path(rel) : File.join(git_root, rel)
137
+ end
138
+ path.start_with?("/") ? path : File.expand_path(path)
139
+ end
140
+
141
+ path = expand.call(new_path)
142
+ return path if path && File.exist?(path)
143
+
144
+ expand.call(old_path)
145
+ end
146
+
147
+ def git_diff_w()
148
+ return if !if_cmd_exists("git")
149
+ diff_buffer_init
150
+
151
+ dir = vma.buf.fname ? File.dirname(vma.buf.fname) : Dir.pwd
152
+ git_root = `git -C #{Shellwords.escape(dir)} rev-parse --show-toplevel 2>/dev/null`.strip
153
+ if git_root.empty?
154
+ message("Not a git repository")
155
+ return
156
+ end
157
+
158
+ bufstr = run_cmd("git -C #{Shellwords.escape(git_root)} diff -w")
159
+ if bufstr.strip.empty?
160
+ message("git diff -w: no changes")
161
+ return
162
+ end
163
+
164
+ create_new_file(nil, bufstr)
165
+ gui_set_file_lang(vma.buf.id, "diff")
166
+ vma.kbd.set_mode(:diffview)
167
+ end
168
+
121
169
  def git_diff_buffer()
122
170
  return if !if_cmd_exists("git")
123
171
  diff_buffer_init
@@ -1,9 +1,5 @@
1
1
  require "pty"
2
2
 
3
- # def handle_drag_and_drop(fname)
4
- # debug "EDITOR:handle_drag_and_drop"
5
- # buf.handle_drag_and_drop(fname)
6
- # end
7
3
 
8
4
  class Editor
9
5
  attr_reader :file_content_search_paths, :file_name_search_paths, :gui, :hook, :macro, :actions
@@ -131,6 +127,22 @@ class Editor
131
127
  #TODO: if language enabled in config?
132
128
  end
133
129
 
130
+ # Load each module that is enabled in settings.
131
+ # Each module lives in modules/<name>/<name>.rb and may optionally define
132
+ # <name>_init to register actions, key bindings, etc.
133
+ Dir.glob(ppath("modules/*/")).sort.each do |mod_dir|
134
+ mod_name = File.basename(mod_dir)
135
+ if cnf.modules.public_send(mod_name).enabled?
136
+ main_file = File.join(mod_dir, "#{mod_name}.rb")
137
+ if File.exist?(main_file)
138
+ load main_file
139
+ # Call <name>_init if the module defines it.
140
+ init_fn = "#{mod_name}_init"
141
+ send(init_fn) if respond_to?(init_fn, true)
142
+ end
143
+ end
144
+ end
145
+
134
146
  fname = nil
135
147
  if cnf.startup_file?
136
148
  fname_ = File.expand_path(cnf.startup_file!)
@@ -171,6 +183,17 @@ class Editor
171
183
  check_session_restore unless argv_has_files
172
184
 
173
185
  @hook.call(:after_init)
186
+
187
+ if ARGV.include?("--test")
188
+ test_files = ARGV.select { |a| a.end_with?(".rb") && File.file?(a) }
189
+ run_as_idle proc {
190
+ test_files.each { |f| load f }
191
+ success = run_vma_tests
192
+ # Defer shutdown so GTK can drain all pending idle callbacks from test
193
+ # teardown before widget destruction begins (avoids heap corruption).
194
+ GLib::Timeout.add(300) { shutdown(); false }
195
+ }
196
+ end
174
197
  end
175
198
 
176
199
  def register_plugin(name, obj)
@@ -206,6 +229,7 @@ class Editor
206
229
 
207
230
  def save_var_to_file(varname, vardata)
208
231
  fn = get_dot_path(varname)
232
+ #TODO: check that save path is safe
209
233
  f = File.open(fn, "w")
210
234
  File.binwrite(f, vardata)
211
235
  f.close
@@ -253,6 +277,7 @@ class Editor
253
277
 
254
278
  fnames = File.read(session_path).lines.map(&:strip).reject(&:empty?)
255
279
  fnames.select! { |f| File.exist?(f) }
280
+ fnames.reject! { |f| vma.buffers.get_buffer_by_filename(f) }
256
281
  return if fnames.empty?
257
282
 
258
283
  n = fnames.size
@@ -404,12 +429,6 @@ def set_next_command_count(num)
404
429
  debug("NEXT COMMAND COUNT: #{$next_command_count}")
405
430
  end
406
431
 
407
- def start_minibuffer_cmd(bufname, bufstr, cmd)
408
- vma.kbd.set_mode(:minibuffer)
409
- $minibuffer = Buffer.new(bufstr, "")
410
- $minibuffer.call_func = method(cmd)
411
- end
412
-
413
432
  def if_cmd_exists(cmd)
414
433
  cmd = cmd.gsub(/[^a-zA-Z0-9_\-]/, '')
415
434
  if !system("which #{cmd} > /dev/null 2>&1")
@@ -419,53 +438,6 @@ def if_cmd_exists(cmd)
419
438
  return true
420
439
  end
421
440
 
422
- def invoke_command()
423
- start_minibuffer_cmd("", "", :execute_command)
424
- end
425
-
426
- def execute_command(input_str)
427
- begin
428
- out_str = eval(input_str, TOPLEVEL_BINDING) #TODO: Other binding?
429
- $minibuffer.clear
430
- $minibuffer << out_str.to_s #TODO: segfaults, why?
431
- rescue SyntaxError
432
- debug("SYNTAX ERROR with eval cmd #{action}: " + $!.to_s)
433
- end
434
- end
435
-
436
- def minibuffer_end()
437
- debug "minibuffer_end"
438
- vma.kbd.set_mode(:command)
439
- minibuffer_input = $minibuffer.to_s[0..-2]
440
- return $minibuffer.call_func.call(minibuffer_input)
441
- end
442
-
443
- def minibuffer_cancel()
444
- debug "minibuffer_cancel"
445
- vma.kbd.set_mode(:command)
446
- minibuffer_input = $minibuffer.to_s[0..-2]
447
- # $minibuffer.call_func.call('')
448
- end
449
-
450
- def minibuffer_new_char(c)
451
- if c == "\r"
452
- raise "Should not come here"
453
- debug "MINIBUFFER END"
454
- else
455
- $minibuffer.insert_txt(c)
456
- debug "MINIBUFFER: #{c}"
457
- end
458
- #vma.buf = $minibuffer
459
- end
460
-
461
- # def readchar_new_char(c)
462
- # $input_char_call_func.call(c)
463
- # end
464
-
465
- def minibuffer_delete()
466
- $minibuffer.delete(BACKWARD_CHAR)
467
- end
468
-
469
441
  def error(str)
470
442
  show_caller
471
443
  debug "#{caller[0]} ERROR: #{str}", 2
@@ -550,10 +522,13 @@ def load_buffer(fname)
550
522
  # If file already open in existing buffer
551
523
  existing_buffer = vma.buffers.get_buffer_by_filename(fname)
552
524
  if existing_buffer != nil
553
- vma.buffers.add_buf_to_history(existing_buffer)
554
525
  return
555
526
  end
556
527
  return if !File.exist?(fname)
528
+ if Encrypt.is_encrypted?(fname)
529
+ decrypt_dialog(filename: fname)
530
+ return nil
531
+ end
557
532
  debug("LOAD BUFFER: #{fname}")
558
533
  buffer = Buffer.new(read_file("", fname), fname)
559
534
  # gui_set_current_buffer(buffer.id)
@@ -562,7 +537,6 @@ def load_buffer(fname)
562
537
  #buf = filter_buffer(buffer)
563
538
  # debug("END FILTER: #{fname}")
564
539
  vma.buffers << buffer
565
- #$buffer_history << vma.buffers.size - 1
566
540
  return buffer
567
541
  end
568
542
 
@@ -631,14 +605,9 @@ def scan_word_start_marks(search_str)
631
605
  return wsmarks
632
606
  end
633
607
 
634
- def hook_draw()
635
- # TODO: as hook.register
636
- # easy_jump_draw()
637
- end
638
-
639
608
  def get_dot_path(sfx)
640
609
  dot_dir = File.expand_path("~/.config/vimamsa")
641
- Dir.mkdir(dot_dir) unless File.exist?(dot_dir)
610
+ FileUtils.mkdir_p(dot_dir) unless File.exist?(dot_dir)
642
611
  dpath = "#{dot_dir}/#{sfx}"
643
612
  return dpath
644
613
  end
@@ -655,6 +624,24 @@ def get_file_line_pointer(s)
655
624
  return nil
656
625
  end
657
626
 
627
+ # Try to resolve a bare "filename.ext:line" reference (no directory component).
628
+ # Searches the current buffer's directory and the last visited directory.
629
+ def resolve_bare_file_line_pointer(s)
630
+ m = s.match(/\A([\w.\-]+\.[a-zA-Z0-9]+):(c?)(\d+)\z/)
631
+ return nil unless m
632
+ fname = m[1]
633
+ col = m[2]
634
+ line = m[3].to_i
635
+ dirs = []
636
+ dirs << File.dirname(vma.buf.fname) if vma.buf&.fname
637
+ dirs << bufs.last_dir if bufs.last_dir
638
+ dirs.uniq.each do |dir|
639
+ candidate = File.join(dir, fname)
640
+ return [candidate, line, col] if File.exist?(candidate)
641
+ end
642
+ nil
643
+ end
644
+
658
645
  def find_project_dir_of_fn(fn)
659
646
  pcomp = Pathname.new(fn).each_filename.to_a
660
647
  parent_dirs = (0..(pcomp.size - 2)).collect { |x| "/" + pcomp[0..x].join("/") }.reverse
@@ -693,9 +680,9 @@ end
693
680
 
694
681
  def save_settings_to_file
695
682
  settings_path = get_dot_path("settings.rb")
696
- lines = SETTINGS_DEFS.flat_map { |section| section[:settings] }.map do |s|
683
+ lines = all_settings_defs.flat_map { |section| section[:settings] }.map do |s|
697
684
  key_str = "cnf." + s[:key].join(".")
698
- val = get(s[:key])
685
+ val = cnf_get(s[:key])
699
686
  "#{key_str} = #{val.inspect}"
700
687
  end
701
688
  IO.write(settings_path, lines.join("\n") + "\n")
@@ -39,12 +39,16 @@ class FileFinder
39
39
 
40
40
  def initialize()
41
41
  vma.hook.register(:shutdown, self.method("save"))
42
- @@dir_list = vma.marshal_load("file_index")
42
+ # Load saved file list on startup (disabled for now, TODO)
43
+ # @@dir_list = vma.marshal_load("file_index")
43
44
  @@dir_list ||= []
44
- update_search_idx
45
+ # update_search_idx
46
+ FileFinder.recursively_find_files
47
+ message("FileFinder initialized, directories: #{cnf.search_dirs!}")
45
48
  end
46
49
 
47
50
  def self.update_search_idx
51
+
48
52
  Thread.new {
49
53
  sleep 0.1
50
54
  @@idx = StringIndex.new