vimamsa 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b44f279b048a7cd7baba9e21e70dc9666a251f9608466f9e4bd38094c486738
4
- data.tar.gz: ef57b1ad00c480d30720ec8d272df38ec289640c7951b8a04a070837e43a261a
3
+ metadata.gz: 0cde9df6e9ba0c6ac7bdbc894726e0fe0941ab50867abfdc150314e2d3a2c3de
4
+ data.tar.gz: b2ca3e49f7b7da7f3f135b12780dc5b2e34e90d5d90bf35ec56ac8efa5f46600
5
5
  SHA512:
6
- metadata.gz: 756f7c861bff64427fac5e397f8e0b4d539f46dd9d72edf39ddbc190eefff09cb86c0107ec38664ee9a1c450c2de9912f6960aee7c990c8b99c4c914cf0f5a56
7
- data.tar.gz: 15a6b06c01f13435a154a2b64c544e8d5b14f253bed3e4b65e845dc510f591fcd77c1e9c278d4cf7b67699205c97952ad6ab8042fffe222260d972b88db42bee
6
+ metadata.gz: 25de014aaef8785f1e169f9afe8242143ff5e70d1c3a71d13aa6556188161d361a7c9248d00f8a530550e488c1dabea4104461011bcb17f135c157e6d9c229e6
7
+ data.tar.gz: 87023b352b98dcceb2e2d388cfe18409bb899522629357bdd62dcb62856738ce5af4f3f5ae4091bac399f251ea4848f524318127e8ad7aa05d8bd49a437b7d48
data/README.md CHANGED
@@ -18,15 +18,21 @@ Vi/Vim -inspired experimental GUI-oriented text editor written with Ruby and GTK
18
18
  - Ruby 3.0+
19
19
  - GTK 4
20
20
 
21
+
21
22
  ## Installation
22
23
 
23
24
 
24
- On Ubuntu:
25
+ On Ubuntu (22.04):
25
26
  ```
26
- sudo apt install ruby-dev
27
+ sudo apt install ruby-dev build-essential
27
28
  sudo gem install vimamsa
28
29
  ```
29
30
 
31
+ Run:
32
+ ```
33
+ vimamsa
34
+ ```
35
+
30
36
  ### Other install options
31
37
 
32
38
  Install from sources:
@@ -51,19 +57,18 @@ vimamsa
51
57
  Install packages for optional features:
52
58
  ```
53
59
  sudo apt install ack-grep clang-format
54
- gem install ripl ripl-multi_line differ parallel listen rufo language_server-protocol
55
60
  ```
56
61
 
57
62
  For customization, edit ~/.vimamsa/custom.rb
58
63
 
59
64
  ## Screenshots
60
65
 
61
- <a href="https://samiddhi.net/vimamsa/screenshot1.png" target="_blank"><img src="https://samiddhi.net/vimamsa/screenshot1.png" width="400"/></a>
62
- <a href="https://samiddhi.net/vimamsa/screenshot2.png" target="_blank"><img src="https://samiddhi.net/vimamsa/screenshot2.png" width="400"/></a>
66
+ <a href="https://raw.githubusercontent.com/SamiSieranoja/vimamsa/refs/heads/master/img/screenshot1.png" target="_blank"><img src="https://raw.githubusercontent.com/SamiSieranoja/vimamsa/refs/heads/master/img/screenshot1.png" width="400"/></a>
67
+ <a href="https://raw.githubusercontent.com/SamiSieranoja/vimamsa/refs/heads/master/img/screenshot2.png" target="_blank"><img src="https://raw.githubusercontent.com/SamiSieranoja/vimamsa/refs/heads/master/img/screenshot2.png" width="400"/></a>
63
68
 
64
69
  ## Key bindings
65
70
 
66
- Key bindings are very much like in VIm. For details, see file lib/vimamsa/key_bindings.rb and lib/vimamsa/key_bindings_vimlike.rb
71
+ Key bindings are very much like in VIm. For details, see menu item "Help -> Show key bindings" and file lib/vimamsa/key_bindings_vimlike.rb
67
72
 
68
73
  Keys that work somewhat similarly as in Vim:
69
74
 
@@ -81,8 +86,9 @@ d y gU gu
81
86
  Keys that work differently to Vim are documented in the tables below
82
87
 
83
88
  Syntax:
84
- ctrl! means press and immediate release of ctrl key. Triggered by key up event.
85
- ctrl-x means press and hold ctrl key, press x
89
+
90
+ - ctrl! means press and immediate release of ctrl key. Triggered by key up event when no other keys were pressed between key down and key up events.
91
+ - ctrl-x means press and hold ctrl key, press x
86
92
 
87
93
  <table>
88
94
  <colgroup>
data/custom_example.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  # Extract unique words
4
4
  # c = Converter.new(lambda { |x| h = {}; x.split(/\s+/).each { |y| h[y] = 1 }; h.keys.join(" ") }, :lambda, :uniqwords)
5
5
 
6
- # Eval selected text as ruby code (e.g. use as calculator)
7
6
 
7
+ # Eval selected text as ruby code (e.g. use as calculator):
8
8
  # bindkey "V , e", "vma.buf.convert_selected_text(:eval)"
9
9
  # syntax: bindkey "mode key1 key2 ..."
10
10
 
@@ -24,6 +24,9 @@
24
24
  # cnf.lsp.server.clangd = { name: "clangd", command: "clangd-12 --offset-encoding=utf-8", type: "stdio" }
25
25
  # cnf.lsp.server.clangd.languages = ["c", "cpp"]
26
26
 
27
+ # Uncomment this if you don't want to see the state trail of previous action
28
+ # on top right corner:
29
+ # cnf.kbd.show_prev_action = false
27
30
 
28
31
 
29
32
 
Binary file
Binary file
data/install.sh ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ gem build vimamsa.gemspec
3
+ sudo gem uninstall vimamsa
4
+ sudo gem install --local $(ls -1tr *gem |head -n 1)
@@ -42,6 +42,8 @@ class Buffer < String
42
42
 
43
43
  @last_save = @last_asked_from_user = @file_last_cheked = Time.now
44
44
  @t_modified = @last_save
45
+ @last_autosave = @last_save
46
+ @autosave_thread = nil
45
47
 
46
48
  @crypt = nil
47
49
  @update_highlight = true
@@ -65,6 +67,7 @@ class Buffer < String
65
67
  if cnf.lsp.enabled?
66
68
  init_lsp
67
69
  end
70
+ start_autosave_timer
68
71
  return self
69
72
  end
70
73
 
@@ -237,11 +240,40 @@ class Buffer < String
237
240
  return words
238
241
  end
239
242
 
243
+ # TODO: review
244
+ def cleanup_deleted_images(deleted_text)
245
+ return if @images.empty?
246
+ return unless deleted_text&.include?("⟦img:")
247
+ deleted_text.scan(/⟦img:(.+?)⟧/) do |match|
248
+ tag_path = match[0]
249
+ abs_path = translate_path(tag_path, self)
250
+ @images.delete_if do |img|
251
+ next false unless img[:path] == abs_path || img[:path] == tag_path
252
+ # img[:obj].destroy unless img[:obj].destroyed?
253
+ # Schedule anchor character deletion after the current delta is done.
254
+ # Using the anchor object lets us find the correct position even if
255
+ # prior edits shifted it.
256
+ anchor = img[:anchor]
257
+ buf_ref = self
258
+ run_as_idle proc {
259
+ next if anchor.nil? || anchor.deleted?
260
+ itr = buf_ref.view.buffer.get_iter_at_child_anchor(anchor)
261
+ next if itr.nil?
262
+ pos = itr.offset
263
+ buf_ref.add_delta([pos, DELETE, 1], true)
264
+ buf_ref.calculate_line_and_column_pos
265
+ }
266
+ true
267
+ end
268
+ end
269
+ end
270
+
240
271
  def add_image(imgpath, pos)
241
272
  return if !is_legal_pos(pos)
242
273
 
243
274
  vbuf = view.buffer
244
275
  itr = vbuf.get_iter_at(:offset => pos)
276
+ return if itr&.child_anchor # anchor already exists here, image already rendered
245
277
  itr2 = vbuf.get_iter_at(:offset => pos + 1)
246
278
  vbuf.delete(itr, itr2)
247
279
  anchor = vbuf.create_child_anchor(itr)
@@ -256,7 +288,7 @@ class Buffer < String
256
288
  da.scale_image
257
289
 
258
290
  # vma.gui.handle_image_resize #TODO:gtk4
259
- @images << { :path => imgpath, :obj => da }
291
+ @images << { :path => imgpath, :obj => da, :anchor => anchor }
260
292
 
261
293
  gui_set_current_buffer(@id) #TODO: needed?
262
294
  end
@@ -308,6 +340,8 @@ class Buffer < String
308
340
  message("Revert buffer #{@fname}")
309
341
  str = read_file("", @fname)
310
342
  self.set_content(str)
343
+ @last_save = Time.now
344
+ refresh_title
311
345
  end
312
346
 
313
347
  def init_encrypted(crypt:, filename:, encrypted:)
@@ -408,7 +442,7 @@ class Buffer < String
408
442
  @title = File.basename(@fname)
409
443
  @dirname = File.dirname(@fname)
410
444
  userhome = File.expand_path("~")
411
- @subtitle = @dirname.gsub(/^#{userhome}/, "~")
445
+ @subtitle = @fname.gsub(/^#{userhome}/, "~")
412
446
  vma.buffers.last_dir = @dirname
413
447
 
414
448
  detect_file_language
@@ -464,6 +498,7 @@ class Buffer < String
464
498
 
465
499
  if delta[1] == DELETE
466
500
  delta[3] = self.slice!(delta[0], delta[2])
501
+ cleanup_deleted_images(delta[3])
467
502
  @deltas << delta
468
503
  update_index(pos, -delta[2])
469
504
  update_line_ends(pos, -delta[2], delta[3])
@@ -485,6 +520,11 @@ class Buffer < String
485
520
  @update_hl_startpos = pos
486
521
  @update_hl_endpos = pos + delta[2]
487
522
  add_hl_update(@update_hl_startpos, @update_hl_endpos)
523
+
524
+ if delta[3]&.include?("⟦img:")
525
+ buf_ref = self
526
+ run_as_idle proc { hpt_scan_images(buf_ref) }
527
+ end
488
528
  end
489
529
  debug("DELTA=#{delta.inspect}", 2)
490
530
  # sanity_check_line_ends #TODO: enable with debug mode
@@ -1045,7 +1085,14 @@ class Buffer < String
1045
1085
  wsm = scan_marks(p - maxws, p, /((?<=\s)\S)|^\S/)
1046
1086
  word_start = wsm[-1]
1047
1087
  word_end = wem[0]
1088
+ elsif boundary == :word2
1089
+ wem = scan_marks(p, p + maxws, /\b/,-1)
1090
+ wsm = scan_marks(p - maxws, p, /\b\w/)
1091
+ word_start = wsm[-1]
1092
+ word_end = wem.select{|x|x>=word_start}[0]
1093
+
1048
1094
  elsif boundary == :word
1095
+ #TODO: change name :word. This works only with autocomplete
1049
1096
  wsm = scan_marks(p - maxws, p, /\b\w/)
1050
1097
  word_start = wsm[-1]
1051
1098
  word_end = p
@@ -1247,10 +1294,12 @@ class Buffer < String
1247
1294
  end
1248
1295
 
1249
1296
  def end_selection()
1250
- @selection_start = nil
1297
+ puts("END SELECTION")
1298
+ # @selection_start = nil
1251
1299
  @selection_active = false
1252
1300
  @visual_mode = false
1253
1301
  #TODO: remove @visual_mode
1302
+ # print(caller())
1254
1303
  end
1255
1304
 
1256
1305
  def start_selection()
@@ -1265,7 +1314,7 @@ class Buffer < String
1265
1314
  end
1266
1315
 
1267
1316
  def copy_active_selection(x = nil)
1268
- debug "!COPY SELECTION"
1317
+ debug "!COPY SELECTION vm=#{@visual_mode}"
1269
1318
  @paste_lines = false
1270
1319
  return if !@visual_mode
1271
1320
 
@@ -1394,7 +1443,7 @@ class Buffer < String
1394
1443
  r = get_visual_mode_range
1395
1444
  if r.begin > r.end
1396
1445
  debug "r.begin > r.end"
1397
- Ripl.start :binding => binding
1446
+ require "pry";binding.pry
1398
1447
  end
1399
1448
  return [r.begin, r.end]
1400
1449
  end
@@ -1412,10 +1461,7 @@ class Buffer < String
1412
1461
  def get_visual_mode_range()
1413
1462
  _start = @selection_start
1414
1463
  _end = @pos
1415
-
1416
1464
  _start, _end = _end, _start if _start > _end
1417
- # _end = _end + 1 if _start < _end #TODO:verify if correct
1418
- # return _start..(_end - 1)
1419
1465
  return _start..(_end)
1420
1466
  end
1421
1467
 
@@ -1567,6 +1613,80 @@ class Buffer < String
1567
1613
  return false
1568
1614
  end
1569
1615
 
1616
+ AUTOSAVE_INTERVAL = 5 # seconds between autosave checks
1617
+
1618
+ def autosave_path
1619
+ return nil if @fname.nil?
1620
+ dir = File.dirname(@fname)
1621
+ base = File.basename(@fname)
1622
+ File.join(dir, ".#{base}_vma_autosave")
1623
+ end
1624
+
1625
+ def autosave
1626
+ return if @fname.nil?
1627
+ return if @t_modified <= @last_autosave
1628
+ apath = autosave_path
1629
+ contents = self.to_s
1630
+ Thread.new {
1631
+ begin
1632
+ File.open(apath, "w+") { |io| io.set_encoding(self.encoding); io.write(contents) }
1633
+ @last_autosave = Time.now
1634
+ debug "autosaved to #{apath}"
1635
+ rescue => ex
1636
+ debug "autosave failed: #{ex}"
1637
+ end
1638
+ }
1639
+ end
1640
+
1641
+ def start_autosave_timer
1642
+ @autosave_thread = Thread.new {
1643
+ loop do
1644
+ sleep AUTOSAVE_INTERVAL
1645
+ autosave
1646
+ end
1647
+ }
1648
+ end
1649
+
1650
+ def delete_autosave_file
1651
+ apath = autosave_path
1652
+ return if apath.nil?
1653
+ File.delete(apath) if File.exist?(apath)
1654
+ rescue => ex
1655
+ debug "delete autosave failed: #{ex}"
1656
+ end
1657
+
1658
+ def check_autosave_load
1659
+ apath = autosave_path
1660
+ return if apath.nil? || !File.exist?(apath)
1661
+ if File.read(apath) == self.to_s
1662
+ delete_autosave_file
1663
+ return
1664
+ end
1665
+ title = "An autosave file was found for <b>#{File.basename(@fname)}</b>. Load autosave?"
1666
+ params = {
1667
+ "title" => title,
1668
+ "inputs" => {
1669
+ "yes_btn" => { :label => "Load", :type => :button, :default_focus => true },
1670
+ "delete_btn" => { :label => "Delete autosave", :type => :button },
1671
+ },
1672
+ :callback => proc { |x| load_autosave_callback(x) },
1673
+ }
1674
+ PopupFormGenerator.new(params).run
1675
+ end
1676
+
1677
+ def load_autosave_callback(x)
1678
+ if x["yes_btn"] == "submit"
1679
+ str = read_file("", autosave_path)
1680
+ set_content(str)
1681
+ @t_modified = Time.now
1682
+ refresh_title
1683
+ message("Loaded autosave for #{@fname}")
1684
+ elsif x["delete_btn"] == "submit"
1685
+ delete_autosave_file
1686
+ message("Deleted autosave for #{@fname}")
1687
+ end
1688
+ end
1689
+
1570
1690
  def save()
1571
1691
  check_if_modified_outside #TODO
1572
1692
  if !@fname
@@ -1575,6 +1695,7 @@ class Buffer < String
1575
1695
  end
1576
1696
  message("Saving file #{@fname}")
1577
1697
  write_contents_to_file(@fname)
1698
+ delete_autosave_file
1578
1699
  hook.call(:file_saved, self)
1579
1700
  end
1580
1701
 
@@ -93,7 +93,7 @@ class Buffer < String
93
93
  if true or running_wayland? and !GLib::Version::or_later?(2, 79, 0)
94
94
  return paste_start_xclip(at, register)
95
95
  end
96
-
96
+
97
97
  clipboard = vma.gui.window.display.clipboard
98
98
  clipboard.read_text_async do |_clipboard, result|
99
99
  begin
@@ -135,6 +135,7 @@ class Buffer < String
135
135
  set_pos(pos + text.size)
136
136
  end
137
137
  set_pos(@pos)
138
+ vma.buf.view.after_action # redraw
138
139
  @clipboard_paste_running = false
139
140
  end
140
141
 
@@ -152,6 +153,22 @@ class Buffer < String
152
153
  return true
153
154
  end
154
155
 
156
+ def increment_current_word()
157
+ debug "increment_current_word", 2
158
+ p = @pos
159
+ return if !is_legal_pos(p)
160
+ (word, range) = get_word_in_pos(p, boundary: :word2)
161
+ if word.match(/(true|false)/i)
162
+ rep = flip_true_false(word)
163
+ else
164
+ num = word.to_i
165
+ num += 1
166
+ rep = num.to_s
167
+ end
168
+ debug [word, range].to_s, 2
169
+ replace_range(range, rep)
170
+ end
171
+
155
172
  def complete_current_word(rep)
156
173
  debug "complete_current_word", 2
157
174
  p = @pos - 1
@@ -217,7 +234,7 @@ class Buffer < String
217
234
 
218
235
  def delete_range(startpos, endpos, x = nil)
219
236
  s = self[startpos..endpos]
220
- if startpos == endpos or s == ""
237
+ if startpos > endpos or s == ""
221
238
  return
222
239
  end
223
240
  if x == :append
@@ -1,6 +1,5 @@
1
1
  # Buffer operations related to cursor position, e.g. moving the cursor (backward, forward, next line etc.)
2
2
  class Buffer < String
3
-
4
3
  def line(lpos)
5
4
  if @line_ends.size == 0
6
5
  return self
@@ -30,15 +29,22 @@ class Buffer < String
30
29
  def refresh_cursor
31
30
  self.view.set_cursor_pos(@pos)
32
31
  end
33
-
32
+
34
33
  def set_pos(new_pos)
34
+ # If user interacts with the buffer, we consider that the buffer has been accessed
35
+ # And navigation has ended
36
+ if new_pos != @pos
37
+ update_access_time
38
+ bufs.reset_navigation
39
+ end
35
40
  if new_pos >= self.size
36
41
  @pos = self.size - 1 # TODO:??right side of last char
37
42
  elsif new_pos >= 0
38
43
  @pos = new_pos
39
44
  end
45
+
40
46
  self.view.set_cursor_pos(pos)
41
- # gui_set_cursor_pos(@id, @pos)
47
+ # gui_set_cursor_pos(@id, @pos)
42
48
  calculate_line_and_column_pos
43
49
 
44
50
  check_if_modified_outside
@@ -141,7 +147,7 @@ class Buffer < String
141
147
  end
142
148
 
143
149
  def jump_to_next_instance_of_word()
144
- if $kbd.last_action == $kbd.cur_action and @current_word != nil
150
+ if vma.kbd.last_action == vma.kbd.cur_action and @current_word != nil
145
151
  # debug "REPEATING *"
146
152
  else
147
153
  start_search = [@pos - 150, 0].max
@@ -42,6 +42,7 @@ class BufferList
42
42
  vma.gui.set_current_buffer(vma.buf.id) #TODO: handle elswhere?
43
43
  # vma.buf.view.set_cursor_pos(vma.buf.pos) #TODO: handle elswhere?
44
44
  update_last_dir(_buf)
45
+ vma.gui.file_panel_refresh
45
46
  end
46
47
 
47
48
  def add(_buf)
@@ -72,6 +73,13 @@ class BufferList
72
73
  end
73
74
  end
74
75
 
76
+ def print_by_access_time
77
+ slist.reverse.each_with_index do |b, i|
78
+ name = b.fname || "(untitled)"
79
+ puts "#{i + 1}. #{b.access_time.strftime('%H:%M:%S')} #{name}"
80
+ end
81
+ end
82
+
75
83
  def get_last_visited_id
76
84
  last_buf = nil
77
85
  for i in 0..(slist.size - 1)
@@ -143,7 +151,9 @@ class BufferList
143
151
 
144
152
  bu.set_active # TODO
145
153
  bu.update_access_time if update_history
154
+ reset_navigation if update_history
146
155
  vma.gui.set_current_buffer(idx)
156
+ vma.gui.file_panel_refresh
147
157
 
148
158
  #TODO: delete?
149
159
  # if !vma.buf.mode_stack.nil? and vma.kbd.get_scope != :editor #TODO
@@ -180,7 +190,7 @@ class BufferList
180
190
  end
181
191
 
182
192
  def get_last_dir
183
- return @last_dir
193
+ return File.expand_path(@last_dir)
184
194
  end
185
195
 
186
196
  def reset_navigation
@@ -255,6 +265,8 @@ class BufferList
255
265
 
256
266
  @list.delete(@h[idx])
257
267
  @h.delete(idx)
268
+ gui_close_buffer(idx)
269
+ vma.gui.file_panel_refresh
258
270
 
259
271
  if auto_open
260
272
  @current_buf = get_last_non_active_buffer
@@ -60,7 +60,7 @@ class BufferManager
60
60
  @@cur = self
61
61
  @header = []
62
62
  @header << "Current buffers:"
63
- @header << "keys: <enter> to select, <c> to close buffer, <x> exit"
63
+ @header << "keys: <enter> (or <double click>) to select, <c> to close buffer, <x> exit"
64
64
  @header << "=" * 40
65
65
 
66
66
  s = ""
data/lib/vimamsa/conf.rb CHANGED
@@ -13,7 +13,7 @@ $confh = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
13
13
  # => $confh = {:foo=>{:bar=>{:baz=>3}}}
14
14
  def set(_id, val)
15
15
  a = $confh
16
- id = _id.to_a
16
+ id = _id.to_a.dup
17
17
  last = id.pop
18
18
  for x in id
19
19
  a = a[x]
@@ -112,6 +112,8 @@ cnf.lsp.enabled = false
112
112
  cnf.fexp.experimental = false
113
113
  cnf.experimental = false
114
114
 
115
+ cnf.kbd.show_prev_action = true
116
+
115
117
  cnf.tab.width = 2
116
118
  cnf.tab.to_spaces_default = false
117
119
  cnf.tab.to_spaces_languages = ["c", "java", "ruby", "hyperplaintext", "php"]
@@ -0,0 +1,137 @@
1
+ # Map a "line number in a unified diff output" to the corresponding
2
+ # line number in the *new/changed file* (the + side).
3
+ #
4
+ # Key idea:
5
+ # @@ -old_start,old_count +new_start,new_count @@
6
+ # sets starting counters. Then walk each hunk line:
7
+ # ' ' => old++, new++
8
+ # '-' => old++
9
+ # '+' => new++
10
+ #
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.
14
+ #
15
+ class DiffLineMapper
16
+ HUNK_RE = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/
17
+
18
+ def initialize(diff_text)
19
+ @lines = diff_text.lines
20
+ end
21
+
22
+ # 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
25
+ def new_line_for_diff_lineno(diff_lineno)
26
+ raise ArgumentError, "diff line number must be >= 1" if diff_lineno.to_i < 1
27
+ idx = diff_lineno.to_i - 1
28
+ return nil if idx >= @lines.length
29
+
30
+ old = nil
31
+ new_ = nil
32
+ in_hunk = false
33
+
34
+ @lines.each_with_index do |line, i|
35
+ if (m = line.match(HUNK_RE))
36
+ old = m[1].to_i
37
+ new_ = m[3].to_i
38
+ in_hunk = true
39
+ next
40
+ end
41
+
42
+ next unless in_hunk
43
+
44
+ if line.start_with?('--- ', '+++ ')
45
+ in_hunk = false
46
+ old = new_ = nil
47
+ next
48
+ end
49
+
50
+ if i == idx
51
+ return nil unless new_
52
+ case line.getbyte(0)
53
+ when '+'.ord then return new_
54
+ when ' '.ord then return new_
55
+ when '-'.ord then return nil
56
+ else return nil
57
+ end
58
+ end
59
+
60
+ next unless old && new_
61
+
62
+ case line.getbyte(0)
63
+ when ' '.ord then old += 1; new_ += 1
64
+ when '-'.ord then old += 1
65
+ when '+'.ord then new_ += 1
66
+ end
67
+ end
68
+
69
+ nil
70
+ end
71
+ end
72
+
73
+ def diff_buffer_init()
74
+ return if @diff_buffer_init_done
75
+ @diff_buffer_init_done = true
76
+ vma.kbd.add_minor_mode("diffview", :diffview, :command)
77
+ bindkey "diffview enter", :diff_buffer_jump_to_source
78
+ end
79
+
80
+ def diff_buffer()
81
+ return if !if_cmd_exists("diff")
82
+ diff_buffer_init
83
+ orig_path = vma.buf.fname
84
+ infile = Tempfile.new("in")
85
+ infile.write(vma.buf.to_s)
86
+ infile.flush
87
+ bufstr = run_cmd("diff -uw '#{orig_path}' #{infile.path}")
88
+ infile.close; infile.unlink
89
+ create_new_file(nil, bufstr)
90
+ gui_set_file_lang(vma.buf.id, "diff")
91
+ vma.kbd.set_mode(:diffview)
92
+ end
93
+
94
+ def diff_buffer_jump_to_source()
95
+ mapper = DiffLineMapper.new(vma.buf.to_s)
96
+ 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
111
+ end
112
+ if orig_path.nil? || !File.exist?(orig_path)
113
+ message("Could not find original file in diff")
114
+ return
115
+ end
116
+
117
+ jump_to_file(orig_path, to_line)
118
+ center_on_current_line
119
+ end
120
+
121
+ def git_diff_buffer()
122
+ return if !if_cmd_exists("git")
123
+ diff_buffer_init
124
+ fname = vma.buf.fname
125
+ if fname.nil?
126
+ message("Buffer has no file")
127
+ return
128
+ end
129
+ bufstr = run_cmd("git diff -w -- #{Shellwords.escape(fname)}")
130
+ if bufstr.strip.empty?
131
+ message("git diff: no changes")
132
+ return
133
+ end
134
+ create_new_file(nil, bufstr)
135
+ gui_set_file_lang(vma.buf.id, "diff")
136
+ vma.kbd.set_mode(:diffview)
137
+ end