ruvim 0.3.0 → 0.4.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +18 -6
  3. data/README.md +15 -1
  4. data/docs/binding.md +16 -0
  5. data/docs/command.md +78 -4
  6. data/docs/config.md +10 -2
  7. data/docs/spec.md +60 -9
  8. data/docs/tutorial.md +24 -0
  9. data/docs/vim_diff.md +18 -8
  10. data/lib/ruvim/app.rb +290 -8
  11. data/lib/ruvim/buffer.rb +14 -2
  12. data/lib/ruvim/cli.rb +6 -0
  13. data/lib/ruvim/editor.rb +12 -1
  14. data/lib/ruvim/file_watcher.rb +243 -0
  15. data/lib/ruvim/git/blame.rb +245 -0
  16. data/lib/ruvim/git/branch.rb +97 -0
  17. data/lib/ruvim/git/commit.rb +102 -0
  18. data/lib/ruvim/git/diff.rb +129 -0
  19. data/lib/ruvim/git/handler.rb +84 -0
  20. data/lib/ruvim/git/log.rb +41 -0
  21. data/lib/ruvim/git/status.rb +103 -0
  22. data/lib/ruvim/global_commands.rb +176 -42
  23. data/lib/ruvim/highlighter.rb +3 -1
  24. data/lib/ruvim/input.rb +1 -0
  25. data/lib/ruvim/lang/diff.rb +41 -0
  26. data/lib/ruvim/lang/json.rb +34 -0
  27. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  28. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  29. data/lib/ruvim/rich_view.rb +16 -0
  30. data/lib/ruvim/screen.rb +9 -12
  31. data/lib/ruvim/version.rb +1 -1
  32. data/lib/ruvim.rb +10 -0
  33. data/test/app_completion_test.rb +25 -0
  34. data/test/app_scenario_test.rb +169 -0
  35. data/test/cli_test.rb +14 -0
  36. data/test/clipboard_test.rb +67 -0
  37. data/test/command_line_test.rb +118 -0
  38. data/test/config_dsl_test.rb +87 -0
  39. data/test/display_width_test.rb +41 -0
  40. data/test/file_watcher_test.rb +197 -0
  41. data/test/follow_test.rb +199 -0
  42. data/test/git_blame_test.rb +713 -0
  43. data/test/highlighter_test.rb +44 -0
  44. data/test/indent_test.rb +86 -0
  45. data/test/rich_view_test.rb +256 -0
  46. data/test/search_option_test.rb +19 -0
  47. data/test/test_helper.rb +9 -0
  48. metadata +17 -1
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RuVim
6
+ module Git
7
+ module Diff
8
+ module_function
9
+
10
+ # Run git diff with optional extra args.
11
+ # Returns [lines, root, error_message].
12
+ def run(file_path, args: [])
13
+ root, err = Git.repo_root(file_path)
14
+ return [nil, nil, err] unless root
15
+
16
+ cmd = ["git", "diff", *args]
17
+ out, err, status = Open3.capture3(*cmd, chdir: root)
18
+ unless status.success?
19
+ return [nil, nil, err.strip]
20
+ end
21
+ [out.lines(chomp: true), root, nil]
22
+ end
23
+
24
+ # Parse diff output to find file and line number at cursor_y.
25
+ # Returns [filename, line_number] or nil.
26
+ def parse_location(lines, cursor_y)
27
+ return nil if lines.empty? || cursor_y < 0 || cursor_y >= lines.length
28
+
29
+ current_file = nil
30
+ new_line = nil
31
+
32
+ (0..cursor_y).each do |i|
33
+ l = lines[i]
34
+ case l
35
+ when /\Adiff --git a\/.+ b\/(.+)/
36
+ current_file = $1
37
+ when /\A\+\+\+ b\/(.+)/
38
+ current_file = $1
39
+ when /\A@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/
40
+ new_line = $1.to_i
41
+ when /\A[ +]/
42
+ # Context or added line: current new_line is this line's number
43
+ new_line += 1 if new_line && i < cursor_y
44
+ end
45
+ # Deleted lines ("-") don't advance new_line
46
+ end
47
+
48
+ return nil unless current_file
49
+
50
+ l = lines[cursor_y]
51
+ case l
52
+ when /\A@@ /
53
+ # On hunk header: new_line already set
54
+ when /\Adiff --git /, /\A---/, /\A\+\+\+/, /\Aindex /
55
+ new_line ||= 1
56
+ when /\A-/
57
+ # Deleted line: point to current new-side position
58
+ end
59
+
60
+ [current_file, new_line || 1]
61
+ end
62
+
63
+ # Command handler methods
64
+ module HandlerMethods
65
+ def git_diff(ctx, argv: [], **)
66
+ file_path = git_resolve_path(ctx)
67
+ unless file_path
68
+ ctx.editor.echo_error("No file or directory to resolve git repo")
69
+ return
70
+ end
71
+
72
+ lines, root, err = Diff.run(file_path, args: argv)
73
+ unless lines
74
+ ctx.editor.echo_error("git diff: #{err}")
75
+ return
76
+ end
77
+
78
+ if lines.empty?
79
+ ctx.editor.echo("No diff output (working tree clean)")
80
+ return
81
+ end
82
+
83
+ buf = ctx.editor.add_virtual_buffer(
84
+ kind: :git_diff,
85
+ name: "[Git Diff]",
86
+ lines: lines,
87
+ filetype: "diff",
88
+ readonly: true,
89
+ modifiable: false
90
+ )
91
+ buf.options["git_repo_root"] = root
92
+ ctx.editor.switch_to_buffer(buf.id)
93
+ bind_git_buffer_keys(ctx.editor, buf.id)
94
+ ctx.editor.echo("[Git Diff]")
95
+ end
96
+
97
+ def git_diff_open_file(ctx, **)
98
+ buf = ctx.buffer
99
+ unless buf.kind == :git_diff || buf.kind == :git_log
100
+ ctx.editor.echo_error("Not a git diff buffer")
101
+ return
102
+ end
103
+
104
+ filename, line_num = Diff.parse_location(buf.lines, ctx.window.cursor_y)
105
+ unless filename
106
+ ctx.editor.echo_error("No file on this line")
107
+ return
108
+ end
109
+
110
+ root = buf.options["git_repo_root"]
111
+ full_path = File.join(root, filename)
112
+ unless File.exist?(full_path)
113
+ ctx.editor.echo_error("File not found: #{filename}")
114
+ return
115
+ end
116
+
117
+ existing = ctx.editor.buffers.values.find { |b| b.path == full_path }
118
+ if existing
119
+ ctx.editor.switch_to_buffer(existing.id)
120
+ else
121
+ new_buf = ctx.editor.add_buffer_from_file(full_path)
122
+ ctx.editor.switch_to_buffer(new_buf.id)
123
+ end
124
+ ctx.editor.current_window.cursor_y = [line_num - 1, 0].max
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RuVim
6
+ module Git
7
+ module_function
8
+
9
+ # Find git repository root from a file path.
10
+ # Returns [root_path, error_message].
11
+ def repo_root(file_path)
12
+ dir = File.directory?(file_path) ? file_path : File.dirname(file_path)
13
+ out, err, status = Open3.capture3("git", "rev-parse", "--show-toplevel", chdir: dir)
14
+ unless status.success?
15
+ return [nil, err.strip]
16
+ end
17
+ [out.strip, nil]
18
+ end
19
+
20
+ module Handler
21
+ GIT_SUBCOMMANDS = {
22
+ "blame" => :git_blame,
23
+ "blameprev" => :git_blame_prev,
24
+ "blameback" => :git_blame_back,
25
+ "blamecommit" => :git_blame_commit,
26
+ "status" => :git_status,
27
+ "diff" => :git_diff,
28
+ "log" => :git_log,
29
+ "branch" => :git_branch,
30
+ "commit" => :git_commit,
31
+ }.freeze
32
+
33
+ include Blame::HandlerMethods
34
+ include Status::HandlerMethods
35
+ include Diff::HandlerMethods
36
+ include Log::HandlerMethods
37
+ include Branch::HandlerMethods
38
+ include Commit::HandlerMethods
39
+
40
+ def enter_git_command_mode(ctx, **)
41
+ ctx.editor.enter_command_line_mode(":")
42
+ ctx.editor.command_line.replace_text("git ")
43
+ ctx.editor.clear_message
44
+ end
45
+
46
+ def ex_git(ctx, argv: [], **)
47
+ sub = argv.first.to_s.downcase
48
+ if sub.empty?
49
+ ctx.editor.echo("Git subcommands: #{GIT_SUBCOMMANDS.keys.join(', ')}")
50
+ return
51
+ end
52
+
53
+ method = GIT_SUBCOMMANDS[sub]
54
+ unless method
55
+ ctx.editor.echo_error("Unknown Git subcommand: #{sub}")
56
+ return
57
+ end
58
+
59
+ public_send(method, ctx, argv: argv[1..], kwargs: {}, bang: false, count: 1)
60
+ end
61
+
62
+ def git_close_buffer(ctx, **)
63
+ buf_id = ctx.buffer.id
64
+ ctx.editor.git_stream_stop_handler&.call(buf_id)
65
+ ctx.editor.delete_buffer(buf_id)
66
+ end
67
+
68
+ private
69
+
70
+ def git_resolve_path(ctx)
71
+ path = ctx.buffer.path
72
+ return path if path && File.exist?(path)
73
+ dir = Dir.pwd
74
+ File.directory?(dir) ? dir : nil
75
+ end
76
+
77
+ def bind_git_buffer_keys(editor, buffer_id)
78
+ km = editor.keymap_manager
79
+ km.bind_buffer(buffer_id, "\e", "git.close_buffer")
80
+ km.bind_buffer(buffer_id, "<C-c>", "git.close_buffer")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Git
5
+ module Log
6
+ # Command handler methods
7
+ module HandlerMethods
8
+ def git_log(ctx, argv: [], **)
9
+ file_path = git_resolve_path(ctx)
10
+ unless file_path
11
+ ctx.editor.echo_error("No file or directory to resolve git repo")
12
+ return
13
+ end
14
+
15
+ root, err = Git.repo_root(file_path)
16
+ unless root
17
+ ctx.editor.echo_error("git log: #{err}")
18
+ return
19
+ end
20
+
21
+ filetype = argv.include?("-p") ? "diff" : nil
22
+ buf = ctx.editor.add_virtual_buffer(
23
+ kind: :git_log,
24
+ name: "[Git Log]",
25
+ lines: [""],
26
+ filetype: filetype,
27
+ readonly: true,
28
+ modifiable: false
29
+ )
30
+ buf.options["git_repo_root"] = root
31
+ ctx.editor.switch_to_buffer(buf.id)
32
+ bind_git_buffer_keys(ctx.editor, buf.id)
33
+ ctx.editor.echo("[Git Log] loading...")
34
+
35
+ cmd = ["git", "log", *argv]
36
+ ctx.editor.git_stream_handler&.call(buf.id, cmd, root)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module RuVim
6
+ module Git
7
+ module Status
8
+ module_function
9
+
10
+ # Run git status.
11
+ # Returns [lines, root, error_message].
12
+ def run(file_path)
13
+ root, err = Git.repo_root(file_path)
14
+ return [nil, nil, err] unless root
15
+
16
+ out, err, status = Open3.capture3("git", "status", chdir: root)
17
+ unless status.success?
18
+ return [nil, nil, err.strip]
19
+ end
20
+ [out.lines(chomp: true), root, nil]
21
+ end
22
+
23
+ # Extract filename from a git status output line.
24
+ # Returns relative path or nil.
25
+ def parse_filename(line)
26
+ stripped = line.to_s.strip
27
+ case stripped
28
+ when /\A(?:modified|new file|deleted|renamed|copied|typechange):\s+(.+)/
29
+ $1.strip
30
+ when /\A(\S.+)/
31
+ # Untracked file lines (no prefix keyword)
32
+ path = $1.strip
33
+ # Skip section headers and hints
34
+ return nil if path.start_with?("(")
35
+ return nil if path.match?(/\A[A-Z]/)
36
+ path
37
+ else
38
+ nil
39
+ end
40
+ end
41
+
42
+ # Command handler methods
43
+ module HandlerMethods
44
+ def git_status(ctx, **)
45
+ file_path = git_resolve_path(ctx)
46
+ unless file_path
47
+ ctx.editor.echo_error("No file or directory to resolve git repo")
48
+ return
49
+ end
50
+
51
+ lines, root, err = Status.run(file_path)
52
+ unless lines
53
+ ctx.editor.echo_error("git status: #{err}")
54
+ return
55
+ end
56
+
57
+ buf = ctx.editor.add_virtual_buffer(
58
+ kind: :git_status,
59
+ name: "[Git Status]",
60
+ lines: lines,
61
+ readonly: true,
62
+ modifiable: false
63
+ )
64
+ buf.options["git_repo_root"] = root
65
+ ctx.editor.switch_to_buffer(buf.id)
66
+ bind_git_buffer_keys(ctx.editor, buf.id)
67
+ ctx.editor.echo("[Git Status]")
68
+ end
69
+
70
+ def git_status_open_file(ctx, **)
71
+ buf = ctx.buffer
72
+ unless buf.kind == :git_status
73
+ ctx.editor.echo_error("Not a git status buffer")
74
+ return
75
+ end
76
+
77
+ line = buf.line_at(ctx.window.cursor_y)
78
+ filename = Status.parse_filename(line)
79
+ unless filename
80
+ ctx.editor.echo_error("No file on this line")
81
+ return
82
+ end
83
+
84
+ root = buf.options["git_repo_root"]
85
+ full_path = File.join(root, filename)
86
+ unless File.exist?(full_path)
87
+ ctx.editor.echo_error("File not found: #{filename}")
88
+ return
89
+ end
90
+
91
+ existing = ctx.editor.buffers.values.find { |b| b.path == full_path }
92
+ if existing
93
+ ctx.editor.switch_to_buffer(existing.id)
94
+ else
95
+ new_buf = ctx.editor.add_buffer_from_file(full_path)
96
+ ctx.editor.switch_to_buffer(new_buf.id)
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end
103
+ end
@@ -467,6 +467,8 @@ module RuVim
467
467
  when "k" then delete_lines_up(ctx, ncount)
468
468
  when "$" then delete_to_end_of_line(ctx)
469
469
  when "w" then delete_word_forward(ctx, ncount)
470
+ when "G" then delete_lines_to_end(ctx)
471
+ when "gg" then delete_lines_to_start(ctx)
470
472
  when "iw" then delete_text_object_word(ctx, around: false)
471
473
  when "aw" then delete_text_object_word(ctx, around: true)
472
474
  else
@@ -478,9 +480,22 @@ module RuVim
478
480
 
479
481
  def change_motion(ctx, count:, kwargs:, **)
480
482
  materialize_intro_buffer_if_needed(ctx)
481
- handled = delete_motion(ctx, count:, kwargs:)
482
- return unless handled
483
+ motion = (kwargs[:motion] || kwargs["motion"]).to_s
484
+ result = delete_motion(ctx, count:, kwargs:)
485
+ return unless result
483
486
 
487
+ if result == :linewise
488
+ case motion
489
+ when "G"
490
+ y = ctx.buffer.lines.length
491
+ ctx.buffer.insert_lines_at(y, [""])
492
+ ctx.window.cursor_y = y
493
+ when "gg"
494
+ ctx.buffer.insert_lines_at(0, [""])
495
+ ctx.window.cursor_y = 0
496
+ end
497
+ ctx.window.cursor_x = 0
498
+ end
484
499
  enter_insert_mode(ctx)
485
500
  end
486
501
 
@@ -610,6 +625,10 @@ module RuVim
610
625
  text = ctx.buffer.span_text(y, x, target[:row], target[:col])
611
626
  store_yank_register(ctx, text:, type: :charwise)
612
627
  ctx.editor.echo("yanked")
628
+ when "G"
629
+ yank_lines_to_end(ctx)
630
+ when "gg"
631
+ yank_lines_to_start(ctx)
613
632
  when "iw"
614
633
  yank_text_object_word(ctx, around: false)
615
634
  when "aw"
@@ -756,6 +775,11 @@ module RuVim
756
775
  end
757
776
 
758
777
  def file_write(ctx, argv:, bang:, **)
778
+ if ctx.buffer.kind == :git_commit
779
+ git_commit_execute(ctx)
780
+ return
781
+ end
782
+
759
783
  path = argv[0]
760
784
  target = ctx.buffer.write_to(path)
761
785
  size = File.exist?(target) ? File.size(target) : 0
@@ -767,6 +791,22 @@ module RuVim
767
791
  end
768
792
 
769
793
  def app_quit(ctx, bang:, **)
794
+ if ctx.buffer.kind == :filter
795
+ saved_y = ctx.buffer.options["filter_source_cursor_y"]
796
+ saved_x = ctx.buffer.options["filter_source_cursor_x"]
797
+ saved_row_offset = ctx.buffer.options["filter_source_row_offset"]
798
+ saved_col_offset = ctx.buffer.options["filter_source_col_offset"]
799
+ ctx.editor.delete_buffer(ctx.buffer.id)
800
+ if saved_y
801
+ win = ctx.editor.current_window
802
+ win.cursor_y = saved_y
803
+ win.cursor_x = saved_x || 0
804
+ win.row_offset = saved_row_offset || 0
805
+ win.col_offset = saved_col_offset || 0
806
+ end
807
+ return
808
+ end
809
+
770
810
  if ctx.editor.window_count > 1
771
811
  ctx.editor.close_current_window
772
812
  ctx.editor.echo("closed window")
@@ -1336,6 +1376,69 @@ module RuVim
1336
1376
  RuVim::RichView.toggle!(ctx.editor)
1337
1377
  end
1338
1378
 
1379
+ def rich_view_close_buffer(ctx, **)
1380
+ ctx.editor.delete_buffer(ctx.buffer.id)
1381
+ end
1382
+
1383
+ def search_filter(ctx, **)
1384
+ editor = ctx.editor
1385
+ search = editor.last_search
1386
+ unless search
1387
+ editor.echo_error("No search pattern")
1388
+ return
1389
+ end
1390
+
1391
+ regex = compile_search_regex(search[:pattern], editor: editor, window: ctx.window, buffer: ctx.buffer)
1392
+ source_buffer = ctx.buffer
1393
+
1394
+ # Collect matching lines with origin mapping
1395
+ origins = []
1396
+ matching_lines = []
1397
+ source_buffer.lines.each_with_index do |line, row|
1398
+ if regex.match?(line)
1399
+ # If source is a filter buffer, chain back to the original
1400
+ if source_buffer.kind == :filter && source_buffer.options["filter_origins"]
1401
+ origins << source_buffer.options["filter_origins"][row]
1402
+ else
1403
+ origins << { buffer_id: source_buffer.id, row: row }
1404
+ end
1405
+ matching_lines << line
1406
+ end
1407
+ end
1408
+
1409
+ if matching_lines.empty?
1410
+ editor.echo_error("Pattern not found: #{search[:pattern]}")
1411
+ return
1412
+ end
1413
+
1414
+ filetype = source_buffer.options["filetype"]
1415
+ filter_buf = editor.add_virtual_buffer(
1416
+ kind: :filter,
1417
+ name: "[Filter: /#{search[:pattern]}/]",
1418
+ lines: matching_lines,
1419
+ filetype: filetype,
1420
+ readonly: false,
1421
+ modifiable: false
1422
+ )
1423
+ filter_buf.options["filter_origins"] = origins
1424
+ filter_buf.options["filter_source_buffer_id"] = source_buffer.id
1425
+ filter_buf.options["filter_source_cursor_y"] = ctx.window.cursor_y
1426
+ filter_buf.options["filter_source_cursor_x"] = ctx.window.cursor_x
1427
+ filter_buf.options["filter_source_row_offset"] = ctx.window.row_offset
1428
+ filter_buf.options["filter_source_col_offset"] = ctx.window.col_offset
1429
+ editor.switch_to_buffer(filter_buf.id)
1430
+ editor.echo("filter: #{matching_lines.length} line(s)")
1431
+ end
1432
+
1433
+ def ex_filter(ctx, argv:, **)
1434
+ if argv.any?
1435
+ pattern = parse_vimgrep_pattern(argv.join(" "))
1436
+ editor = ctx.editor
1437
+ editor.set_last_search(pattern: pattern, direction: :forward)
1438
+ end
1439
+ search_filter(ctx)
1440
+ end
1441
+
1339
1442
  def submit_search(ctx, pattern:, direction:)
1340
1443
  text = pattern.to_s
1341
1444
  if text.empty?
@@ -1349,6 +1452,8 @@ module RuVim
1349
1452
  move_to_search(ctx, pattern: text, direction:, count: 1)
1350
1453
  end
1351
1454
 
1455
+ include RuVim::Git::Handler
1456
+
1352
1457
  private
1353
1458
 
1354
1459
  def reindent_range(ctx, start_row, end_row)
@@ -1723,6 +1828,48 @@ module RuVim
1723
1828
  true
1724
1829
  end
1725
1830
 
1831
+ def delete_lines_to_end(ctx)
1832
+ y = ctx.window.cursor_y
1833
+ total = ctx.buffer.lines.length - y
1834
+ deleted = ctx.buffer.line_block_text(y, total)
1835
+ ctx.buffer.begin_change_group
1836
+ total.times { ctx.buffer.delete_line(y) }
1837
+ ctx.buffer.end_change_group
1838
+ store_delete_register(ctx, text: deleted, type: :linewise)
1839
+ ctx.window.clamp_to_buffer(ctx.buffer)
1840
+ :linewise
1841
+ end
1842
+
1843
+ def delete_lines_to_start(ctx)
1844
+ y = ctx.window.cursor_y
1845
+ total = y + 1
1846
+ deleted = ctx.buffer.line_block_text(0, total)
1847
+ ctx.buffer.begin_change_group
1848
+ total.times { ctx.buffer.delete_line(0) }
1849
+ ctx.buffer.end_change_group
1850
+ store_delete_register(ctx, text: deleted, type: :linewise)
1851
+ ctx.window.cursor_y = 0
1852
+ ctx.window.cursor_x = 0
1853
+ ctx.window.clamp_to_buffer(ctx.buffer)
1854
+ :linewise
1855
+ end
1856
+
1857
+ def yank_lines_to_end(ctx)
1858
+ y = ctx.window.cursor_y
1859
+ total = ctx.buffer.lines.length - y
1860
+ text = ctx.buffer.line_block_text(y, total)
1861
+ store_yank_register(ctx, text: text, type: :linewise)
1862
+ ctx.editor.echo("#{total} line(s) yanked")
1863
+ end
1864
+
1865
+ def yank_lines_to_start(ctx)
1866
+ y = ctx.window.cursor_y
1867
+ total = y + 1
1868
+ text = ctx.buffer.line_block_text(0, total)
1869
+ store_yank_register(ctx, text: text, type: :linewise)
1870
+ ctx.editor.echo("#{total} line(s) yanked")
1871
+ end
1872
+
1726
1873
  def delete_to_end_of_line(ctx)
1727
1874
  y = ctx.window.cursor_y
1728
1875
  x = ctx.window.cursor_x
@@ -1754,22 +1901,22 @@ module RuVim
1754
1901
  end
1755
1902
 
1756
1903
  def delete_text_object_word(ctx, around:)
1757
- span = word_object_span(ctx.buffer, ctx.window, around:)
1758
- return false unless span
1759
-
1760
- text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
1761
- ctx.buffer.begin_change_group
1762
- ctx.buffer.delete_span(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
1763
- ctx.buffer.end_change_group
1764
- store_delete_register(ctx, text:, type: :charwise) unless text.empty?
1765
- ctx.window.cursor_y = span[:start_row]
1766
- ctx.window.cursor_x = span[:start_col]
1767
- ctx.window.clamp_to_buffer(ctx.buffer)
1768
- true
1904
+ delete_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
1769
1905
  end
1770
1906
 
1771
1907
  def delete_text_object(ctx, motion)
1772
- span = text_object_span(ctx.buffer, ctx.window, motion)
1908
+ delete_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
1909
+ end
1910
+
1911
+ def yank_text_object_word(ctx, around:)
1912
+ yank_span(ctx, word_object_span(ctx.buffer, ctx.window, around:))
1913
+ end
1914
+
1915
+ def yank_text_object(ctx, motion)
1916
+ yank_span(ctx, text_object_span(ctx.buffer, ctx.window, motion))
1917
+ end
1918
+
1919
+ def delete_span(ctx, span)
1773
1920
  return false unless span
1774
1921
 
1775
1922
  text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
@@ -1783,18 +1930,7 @@ module RuVim
1783
1930
  true
1784
1931
  end
1785
1932
 
1786
- def yank_text_object_word(ctx, around:)
1787
- span = word_object_span(ctx.buffer, ctx.window, around:)
1788
- return false unless span
1789
-
1790
- text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
1791
- store_yank_register(ctx, text:, type: :charwise) unless text.empty?
1792
- ctx.editor.echo("yanked")
1793
- true
1794
- end
1795
-
1796
- def yank_text_object(ctx, motion)
1797
- span = text_object_span(ctx.buffer, ctx.window, motion)
1933
+ def yank_span(ctx, span)
1798
1934
  return false unless span
1799
1935
 
1800
1936
  text = ctx.buffer.span_text(span[:start_row], span[:start_col], span[:end_row], span[:end_col])
@@ -1979,9 +2115,9 @@ module RuVim
1979
2115
  x = [window.cursor_x, line.length - 1].min
1980
2116
  return nil if x.negative?
1981
2117
 
1982
- left = find_left_quote(line, x, quote)
2118
+ left = find_quote(line, x, quote, :left)
1983
2119
  right_from = [x, (left ? left + 1 : 0)].max
1984
- right = find_right_quote(line, right_from, quote)
2120
+ right = find_quote(line, right_from, quote, :right)
1985
2121
  return nil unless left && right && left < right
1986
2122
 
1987
2123
  if around
@@ -2053,20 +2189,18 @@ module RuVim
2053
2189
  end
2054
2190
  end
2055
2191
 
2056
- def find_left_quote(line, x, quote)
2192
+ def find_quote(line, x, quote, direction)
2057
2193
  i = x
2058
- while i >= 0
2059
- return i if line[i] == quote && !escaped?(line, i)
2060
- i -= 1
2061
- end
2062
- nil
2063
- end
2064
-
2065
- def find_right_quote(line, x, quote)
2066
- i = x
2067
- while i < line.length
2068
- return i if line[i] == quote && !escaped?(line, i)
2069
- i += 1
2194
+ if direction == :left
2195
+ while i >= 0
2196
+ return i if line[i] == quote && !escaped?(line, i)
2197
+ i -= 1
2198
+ end
2199
+ else
2200
+ while i < line.length
2201
+ return i if line[i] == quote && !escaped?(line, i)
2202
+ i += 1
2203
+ end
2070
2204
  end
2071
2205
  nil
2072
2206
  end
@@ -19,12 +19,14 @@ module RuVim
19
19
  case ft
20
20
  when "ruby"
21
21
  Lang::Ruby.color_columns(text)
22
- when "json"
22
+ when "json", "jsonl"
23
23
  Lang::Json.color_columns(text)
24
24
  when "markdown"
25
25
  Lang::Markdown.color_columns(text)
26
26
  when "scheme"
27
27
  Lang::Scheme.color_columns(text)
28
+ when "diff"
29
+ Lang::Diff.color_columns(text)
28
30
  else
29
31
  {}
30
32
  end
data/lib/ruvim/input.rb CHANGED
@@ -23,6 +23,7 @@ module RuVim
23
23
  when "\u0004" then :ctrl_d
24
24
  when "\u0005" then :ctrl_e
25
25
  when "\u0006" then :ctrl_f
26
+ when "\u0007" then :ctrl_g
26
27
  when "\u0009" then :ctrl_i
27
28
  when "\u000c" then :ctrl_l
28
29
  when "\u000e" then :ctrl_n