ruvim 0.3.0 → 0.6.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +68 -7
  3. data/README.md +30 -7
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +18 -1
  10. data/docs/command.md +156 -10
  11. data/docs/config.md +10 -2
  12. data/docs/done.md +23 -0
  13. data/docs/spec.md +162 -25
  14. data/docs/todo.md +9 -0
  15. data/docs/tutorial.md +33 -1
  16. data/docs/vim_diff.md +31 -8
  17. data/ext/ruvim/extconf.rb +5 -0
  18. data/ext/ruvim/ruvim_ext.c +519 -0
  19. data/lib/ruvim/app.rb +246 -2525
  20. data/lib/ruvim/browser.rb +104 -0
  21. data/lib/ruvim/buffer.rb +43 -20
  22. data/lib/ruvim/cli.rb +6 -0
  23. data/lib/ruvim/command_invocation.rb +2 -2
  24. data/lib/ruvim/completion_manager.rb +708 -0
  25. data/lib/ruvim/dispatcher.rb +14 -8
  26. data/lib/ruvim/display_width.rb +91 -45
  27. data/lib/ruvim/editor.rb +74 -80
  28. data/lib/ruvim/ex_command_registry.rb +3 -1
  29. data/lib/ruvim/file_watcher.rb +243 -0
  30. data/lib/ruvim/gh/link.rb +207 -0
  31. data/lib/ruvim/git/blame.rb +255 -0
  32. data/lib/ruvim/git/branch.rb +112 -0
  33. data/lib/ruvim/git/commit.rb +102 -0
  34. data/lib/ruvim/git/diff.rb +129 -0
  35. data/lib/ruvim/git/grep.rb +107 -0
  36. data/lib/ruvim/git/handler.rb +125 -0
  37. data/lib/ruvim/git/log.rb +41 -0
  38. data/lib/ruvim/git/status.rb +103 -0
  39. data/lib/ruvim/global_commands.rb +351 -77
  40. data/lib/ruvim/highlighter.rb +4 -11
  41. data/lib/ruvim/input.rb +1 -0
  42. data/lib/ruvim/key_handler.rb +1510 -0
  43. data/lib/ruvim/keymap_manager.rb +7 -7
  44. data/lib/ruvim/lang/base.rb +5 -0
  45. data/lib/ruvim/lang/c.rb +116 -0
  46. data/lib/ruvim/lang/cpp.rb +107 -0
  47. data/lib/ruvim/lang/csv.rb +4 -1
  48. data/lib/ruvim/lang/diff.rb +43 -0
  49. data/lib/ruvim/lang/dockerfile.rb +36 -0
  50. data/lib/ruvim/lang/elixir.rb +85 -0
  51. data/lib/ruvim/lang/erb.rb +30 -0
  52. data/lib/ruvim/lang/go.rb +83 -0
  53. data/lib/ruvim/lang/html.rb +34 -0
  54. data/lib/ruvim/lang/javascript.rb +83 -0
  55. data/lib/ruvim/lang/json.rb +40 -0
  56. data/lib/ruvim/lang/lua.rb +76 -0
  57. data/lib/ruvim/lang/makefile.rb +36 -0
  58. data/lib/ruvim/lang/markdown.rb +3 -4
  59. data/lib/ruvim/lang/ocaml.rb +77 -0
  60. data/lib/ruvim/lang/perl.rb +91 -0
  61. data/lib/ruvim/lang/python.rb +85 -0
  62. data/lib/ruvim/lang/registry.rb +102 -0
  63. data/lib/ruvim/lang/ruby.rb +7 -0
  64. data/lib/ruvim/lang/rust.rb +95 -0
  65. data/lib/ruvim/lang/scheme.rb +5 -0
  66. data/lib/ruvim/lang/sh.rb +76 -0
  67. data/lib/ruvim/lang/sql.rb +52 -0
  68. data/lib/ruvim/lang/toml.rb +36 -0
  69. data/lib/ruvim/lang/tsv.rb +4 -1
  70. data/lib/ruvim/lang/typescript.rb +53 -0
  71. data/lib/ruvim/lang/yaml.rb +62 -0
  72. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  73. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  74. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  75. data/lib/ruvim/rich_view.rb +30 -7
  76. data/lib/ruvim/screen.rb +135 -84
  77. data/lib/ruvim/stream/file_load.rb +85 -0
  78. data/lib/ruvim/stream/follow.rb +40 -0
  79. data/lib/ruvim/stream/git.rb +43 -0
  80. data/lib/ruvim/stream/run.rb +74 -0
  81. data/lib/ruvim/stream/stdin.rb +55 -0
  82. data/lib/ruvim/stream.rb +35 -0
  83. data/lib/ruvim/stream_mixer.rb +394 -0
  84. data/lib/ruvim/terminal.rb +18 -4
  85. data/lib/ruvim/text_metrics.rb +84 -65
  86. data/lib/ruvim/version.rb +1 -1
  87. data/lib/ruvim/window.rb +5 -5
  88. data/lib/ruvim.rb +31 -4
  89. data/test/app_command_test.rb +382 -0
  90. data/test/app_completion_test.rb +65 -16
  91. data/test/app_dot_repeat_test.rb +27 -3
  92. data/test/app_ex_command_test.rb +154 -0
  93. data/test/app_motion_test.rb +13 -12
  94. data/test/app_register_test.rb +2 -1
  95. data/test/app_scenario_test.rb +182 -8
  96. data/test/app_startup_test.rb +70 -27
  97. data/test/app_text_object_test.rb +2 -1
  98. data/test/app_unicode_behavior_test.rb +3 -2
  99. data/test/browser_test.rb +88 -0
  100. data/test/buffer_test.rb +24 -0
  101. data/test/cli_test.rb +77 -0
  102. data/test/clipboard_test.rb +67 -0
  103. data/test/command_invocation_test.rb +33 -0
  104. data/test/command_line_test.rb +118 -0
  105. data/test/config_dsl_test.rb +134 -0
  106. data/test/dispatcher_test.rb +74 -4
  107. data/test/display_width_test.rb +41 -0
  108. data/test/ex_command_registry_test.rb +106 -0
  109. data/test/file_watcher_test.rb +197 -0
  110. data/test/follow_test.rb +198 -0
  111. data/test/gh_link_test.rb +141 -0
  112. data/test/git_blame_test.rb +792 -0
  113. data/test/git_grep_test.rb +64 -0
  114. data/test/highlighter_test.rb +169 -0
  115. data/test/indent_test.rb +223 -0
  116. data/test/input_screen_integration_test.rb +1 -1
  117. data/test/keyword_chars_test.rb +85 -0
  118. data/test/lang_test.rb +634 -0
  119. data/test/markdown_renderer_test.rb +5 -5
  120. data/test/on_save_hook_test.rb +12 -8
  121. data/test/render_snapshot_test.rb +78 -0
  122. data/test/rich_view_test.rb +279 -23
  123. data/test/run_command_test.rb +307 -0
  124. data/test/screen_test.rb +68 -5
  125. data/test/search_option_test.rb +19 -0
  126. data/test/stream_test.rb +165 -0
  127. data/test/test_helper.rb +9 -0
  128. data/test/window_test.rb +59 -0
  129. metadata +68 -2
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark file loading hotspots
5
+ #
6
+ # Usage: ruby benchmark/file_load.rb [path]
7
+ # Default: huge_file in project root
8
+
9
+ require "benchmark"
10
+
11
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
12
+ require "ruvim/buffer"
13
+ require "ruvim/lang/registry"
14
+ require "ruvim/lang/base"
15
+
16
+ FILE = ARGV[0] || File.expand_path("../huge_file", __dir__)
17
+ unless File.exist?(FILE)
18
+ abort "File not found: #{FILE}\nUsage: ruby benchmark/file_load.rb [path]"
19
+ end
20
+
21
+ size_mb = File.size(FILE) / 1024.0 / 1024.0
22
+ puts "File Load Benchmark"
23
+ puts "=" * 60
24
+ puts "Ruby: #{RUBY_VERSION}"
25
+ puts "File: #{FILE}"
26
+ puts "Size: %.1f MB" % size_mb
27
+ puts
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # 1. Raw IO read
31
+ # ---------------------------------------------------------------------------
32
+ puts "--- 1. Raw file read ---"
33
+ Benchmark.bm(25) do |x|
34
+ x.report("File.binread") do
35
+ data = File.binread(FILE)
36
+ data.size # force read
37
+ end
38
+ end
39
+ puts
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # 2. decode_text (encoding detection)
43
+ # ---------------------------------------------------------------------------
44
+ puts "--- 2. decode_text ---"
45
+ raw = File.binread(FILE)
46
+ Benchmark.bm(25) do |x|
47
+ x.report("decode_text") do
48
+ RuVim::Buffer.decode_text(raw)
49
+ end
50
+ end
51
+ puts
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # 3. split_lines (String#split)
55
+ # ---------------------------------------------------------------------------
56
+ puts "--- 3. split_lines ---"
57
+ decoded = RuVim::Buffer.decode_text(raw)
58
+ Benchmark.bm(25) do |x|
59
+ x.report("split(\"\\n\", -1)") do
60
+ decoded.split("\n", -1)
61
+ end
62
+ end
63
+ puts
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # 4. Full Buffer.from_file
67
+ # ---------------------------------------------------------------------------
68
+ puts "--- 4. Full Buffer.from_file ---"
69
+ Benchmark.bm(25) do |x|
70
+ x.report("Buffer.from_file") do
71
+ RuVim::Buffer.from_file(id: 1, path: FILE)
72
+ end
73
+ end
74
+ puts
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # 5. Chunked read (simulating Stream::FileLoad)
78
+ # ---------------------------------------------------------------------------
79
+ puts "--- 5. Chunked read (4MB flush, simulating async load) ---"
80
+ CHUNK = 1 * 1024 * 1024
81
+ FLUSH = 4 * 1024 * 1024
82
+ Benchmark.bm(25) do |x|
83
+ x.report("chunked read+split") do
84
+ io = File.open(FILE, "rb")
85
+ lines = [""]
86
+ pending = "".b
87
+ begin
88
+ loop do
89
+ chunk = io.readpartial(CHUNK)
90
+ pending << chunk
91
+ next if pending.bytesize < FLUSH
92
+
93
+ last_nl = pending.rindex("\n".b)
94
+ if last_nl
95
+ send_bytes = pending[0..last_nl]
96
+ pending = pending[(last_nl + 1)..] || "".b
97
+ else
98
+ send_bytes = pending
99
+ pending = "".b
100
+ end
101
+ decoded = RuVim::Buffer.decode_text(send_bytes)
102
+ parts = decoded.split("\n", -1)
103
+ head = parts.shift || ""
104
+ lines[-1] = lines[-1] + head unless head.empty?
105
+ lines.concat(parts) unless parts.empty?
106
+ end
107
+ rescue EOFError
108
+ unless pending.empty?
109
+ decoded = RuVim::Buffer.decode_text(pending)
110
+ parts = decoded.split("\n", -1)
111
+ head = parts.shift || ""
112
+ lines[-1] = lines[-1] + head unless head.empty?
113
+ lines.concat(parts) unless parts.empty?
114
+ end
115
+ ensure
116
+ io.close
117
+ end
118
+ puts " lines loaded: #{lines.size}"
119
+ end
120
+ end
121
+ puts
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # 6. Memory profile
125
+ # ---------------------------------------------------------------------------
126
+ puts "--- 6. Memory usage ---"
127
+ GC.start
128
+ before = GC.stat[:malloc_increase_bytes_limit]
129
+ mem_before = `ps -o rss= -p #{$$}`.strip.to_i
130
+ buf = RuVim::Buffer.from_file(id: 2, path: FILE)
131
+ GC.start
132
+ mem_after = `ps -o rss= -p #{$$}`.strip.to_i
133
+ puts " Lines: #{buf.line_count}"
134
+ puts " RSS before: #{mem_before / 1024} MB"
135
+ puts " RSS after: #{mem_after / 1024} MB"
136
+ puts " RSS delta: #{(mem_after - mem_before) / 1024} MB"
137
+
138
+ puts
139
+ puts "=" * 60
140
+ puts "Done."
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark for C-extension candidate hotspots in RuVim
5
+ #
6
+ # Usage: ruby benchmark/hotspots.rb
7
+
8
+ require "benchmark"
9
+
10
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
11
+ require "ruvim/display_width"
12
+ require "ruvim/text_metrics"
13
+ require "ruvim/highlighter"
14
+ require "ruvim/lang/registry"
15
+ require "ruvim/lang/base"
16
+ require "ruvim/lang/ruby"
17
+ require "ruvim/lang/c"
18
+ require "ruvim/lang/markdown"
19
+ require "ruvim/lang/json"
20
+ require "ruvim/lang/javascript"
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Test data
24
+ # ---------------------------------------------------------------------------
25
+ ASCII_LINE = ' def foo(bar, baz) = bar + baz # comment with some padding text here!!!' * 2
26
+ CJK_LINE = "日本語テキスト 漢字カタカナ テスト用の行 全角文字を含む行" * 2
27
+ MIXED_LINE = "Hello 世界! def foo(x) = x + 1 # コメント 🚀 emoji test"
28
+ EMOJI_LINE = "🎉🔥💡🚀✨🎊🌟💎🏆🎯" * 5
29
+ TAB_LINE = "\t\tif (x > 0) {\n\t\t\treturn x;\n\t\t}"
30
+
31
+ RUBY_LINE = ' def initialize(name, value: nil) # keyword args'
32
+ C_LINE = ' int *ptr = malloc(sizeof(struct node)); /* alloc */'
33
+ MD_LINE = '## Heading with **bold** and `code` and [link](url)'
34
+ JSON_LINE = ' {"key": "value", "number": 42, "bool": true, "null": null}'
35
+ JS_LINE = ' const fn = async (x) => { return await fetch(url); };'
36
+
37
+ LINES_80COL = [ASCII_LINE, CJK_LINE, MIXED_LINE, RUBY_LINE, C_LINE].freeze
38
+ SCREEN_WIDTH = 120
39
+ SCREEN_ROWS = 50
40
+
41
+ N_ITER = 10_000
42
+ N_RENDER = 2_000
43
+
44
+ puts "RuVim Hotspot Benchmark"
45
+ puts "=" * 60
46
+ puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})"
47
+ puts "Iterations: #{N_ITER} (width), #{N_RENDER} (render)"
48
+ puts
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # 1. DisplayWidth.cell_width — per-character width lookup
52
+ # ---------------------------------------------------------------------------
53
+ puts "--- 1. DisplayWidth.cell_width ---"
54
+ chars = {
55
+ "ASCII 'A'" => "A",
56
+ "CJK '漢'" => "漢",
57
+ "Emoji '🚀'" => "🚀",
58
+ "Combining" => "\u0301", # combining acute
59
+ "Tab" => "\t",
60
+ }
61
+ Benchmark.bm(20) do |x|
62
+ chars.each do |label, ch|
63
+ x.report(label) { N_ITER.times { RuVim::DisplayWidth.cell_width(ch) } }
64
+ end
65
+ end
66
+ puts
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # 2. DisplayWidth.display_width — whole-line width
70
+ # ---------------------------------------------------------------------------
71
+ puts "--- 2. DisplayWidth.display_width ---"
72
+ lines = {
73
+ "ASCII (140c)" => ASCII_LINE,
74
+ "CJK (56c)" => CJK_LINE,
75
+ "Mixed" => MIXED_LINE,
76
+ "Emoji (50c)" => EMOJI_LINE,
77
+ "Tabs" => TAB_LINE,
78
+ }
79
+ Benchmark.bm(20) do |x|
80
+ lines.each do |label, line|
81
+ x.report(label) { N_ITER.times { RuVim::DisplayWidth.display_width(line) } }
82
+ end
83
+ end
84
+ puts
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # 3. TextMetrics.clip_cells_for_width — cell array construction
88
+ # ---------------------------------------------------------------------------
89
+ puts "--- 3. TextMetrics.clip_cells_for_width ---"
90
+ Benchmark.bm(20) do |x|
91
+ lines.each do |label, line|
92
+ x.report(label) { N_RENDER.times { RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH) } }
93
+ end
94
+ end
95
+ puts
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # 4. TextMetrics.char_index_for_screen_col — cursor positioning
99
+ # ---------------------------------------------------------------------------
100
+ puts "--- 4. TextMetrics.char_index_for_screen_col ---"
101
+ Benchmark.bm(20) do |x|
102
+ lines.each do |label, line|
103
+ target = RuVim::DisplayWidth.display_width(line) / 2
104
+ x.report(label) { N_ITER.times { RuVim::TextMetrics.char_index_for_screen_col(line, target) } }
105
+ end
106
+ end
107
+ puts
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # 5. Highlighter.color_columns — syntax highlighting
111
+ # ---------------------------------------------------------------------------
112
+ puts "--- 5. Highlighter.color_columns ---"
113
+ highlight_cases = {
114
+ "Ruby" => ["ruby", RUBY_LINE],
115
+ "C" => ["c", C_LINE],
116
+ "Markdown" => ["markdown", MD_LINE],
117
+ "JSON" => ["json", JSON_LINE],
118
+ "JavaScript" => ["javascript", JS_LINE],
119
+ }
120
+ Benchmark.bm(20) do |x|
121
+ highlight_cases.each do |label, (ft, line)|
122
+ x.report(label) { N_RENDER.times { RuVim::Highlighter.color_columns(ft, line) } }
123
+ end
124
+ end
125
+ puts
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # 6. Simulated full-screen render pass (clip + highlight per line)
129
+ # ---------------------------------------------------------------------------
130
+ puts "--- 6. Simulated screen render (#{SCREEN_ROWS} lines × #{SCREEN_WIDTH} cols) ---"
131
+ screen_lines = Array.new(SCREEN_ROWS) { |i| LINES_80COL[i % LINES_80COL.size] }
132
+ Benchmark.bm(20) do |x|
133
+ x.report("clip_cells only") do
134
+ N_RENDER.times do
135
+ screen_lines.each { |line| RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH) }
136
+ end
137
+ end
138
+ x.report("highlight only") do
139
+ N_RENDER.times do
140
+ screen_lines.each_with_index do |line, i|
141
+ ft = %w[ruby c markdown json javascript][i % 5]
142
+ RuVim::Highlighter.color_columns(ft, line)
143
+ end
144
+ end
145
+ end
146
+ x.report("clip + highlight") do
147
+ N_RENDER.times do
148
+ screen_lines.each_with_index do |line, i|
149
+ ft = %w[ruby c markdown json javascript][i % 5]
150
+ RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH)
151
+ RuVim::Highlighter.color_columns(ft, line)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ puts
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # 7. DisplayWidth.expand_tabs
160
+ # ---------------------------------------------------------------------------
161
+ puts "--- 7. DisplayWidth.expand_tabs ---"
162
+ tab_lines = {
163
+ "2 tabs" => "\t\tcode here",
164
+ "mixed" => "abc\tdef\tghi\tjkl",
165
+ "heavy" => "\t" * 20 + "end",
166
+ }
167
+ Benchmark.bm(20) do |x|
168
+ tab_lines.each do |label, line|
169
+ x.report(label) { N_ITER.times { RuVim::DisplayWidth.expand_tabs(line) } }
170
+ end
171
+ end
172
+ puts
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Summary
176
+ # ---------------------------------------------------------------------------
177
+ puts "=" * 60
178
+ puts "Done. Compare user+sys times to identify C-extension priorities."
data/docs/binding.md CHANGED
@@ -30,6 +30,14 @@
30
30
  - `/` : 前方検索入力に入る
31
31
  - `?` : 後方検索入力に入る
32
32
  - `x` : カーソル位置の文字削除
33
+ - `X` : カーソル前の文字削除
34
+ - `s` : カーソル位置の文字を削除して Insert mode(substitute)
35
+ - `~` : カーソル位置の文字の大文字小文字を切替
36
+ - `J` : 現在行と次の行を結合
37
+ - `D` : カーソルから行末まで削除
38
+ - `C` : カーソルから行末まで変更(削除して Insert mode)
39
+ - `S` : 行全体を変更(削除して Insert mode)
40
+ - `Y` : 行全体をヤンク
33
41
  - `dd` : 現在行を削除
34
42
  - `d` + motion : operator-pending delete(`dw`, `dj`, `dk`, `d$`, `dh`, `dl`)
35
43
  - `diw` / `daw` : 単語 text object delete(簡易)
@@ -68,6 +76,9 @@
68
76
  - `Ctrl-d` / `Ctrl-u` : 半ページ下/上へ移動(概ね表示高さの半分)
69
77
  - `Ctrl-f` / `Ctrl-b` : 1ページ下/上へ移動(`PageDown` / `PageUp` 相当)
70
78
  - `Ctrl-e` / `Ctrl-y` : カーソル位置をなるべく保ったまま画面を1行下/上へスクロール(最小実装)
79
+ - `zt` : カーソル行を画面上端に移動
80
+ - `zz` : カーソル行を画面中央に移動
81
+ - `zb` : カーソル行を画面下端に移動
71
82
  - `q{reg}` : macro 記録開始/終了(再度 `q` で停止)
72
83
  - `@{reg}` / `@@` : macro 再生 / 直前 macro 再生
73
84
  - `n` : 直前検索を次へ
@@ -77,6 +88,12 @@
77
88
  - `gf` : カーソル下のファイル名を開く(最小。`path` / `suffixesadd` を参照)
78
89
  - `file:line` 形式なら開いた後に `line` 行目へ移動
79
90
  - `gr` : Rich mode をトグル(TSV/CSV をテーブル整形表示、同一バッファ上で動作)
91
+ - `g/` : 検索パターンにマッチする行だけを集めたフィルタバッファを作成(再帰フィルタ可、Enter で元行へジャンプ、`:q` で戻る)
92
+ - `Ctrl-g` : Git コマンドモード(`:git ` がプリセットされたコマンドラインに入る)
93
+ - `blame` → Git blame を開く / `status` → Git status / `diff` → Git diff / `log` → Git log / `branch` → ブランチ一覧
94
+ - Blame バッファ内: `p` 親コミットの blame へ遷移 / `P` 前の blame へ戻る / `c` コミット詳細を表示
95
+ - Branch バッファ内: `Enter` で `:git checkout <branch>` をコマンドラインにプリセット(即時実行ではなく確認ステップあり)
96
+ - `Ctrl-c` : stdin stream 停止(stdin バッファ表示時)
80
97
  - `Esc` : メッセージ/保留入力のクリア
81
98
  - `Ctrl-z` : shell へ suspend(`fg` で復帰)
82
99
  - `矢印キー` : 移動
@@ -132,7 +149,7 @@
132
149
  - `Backspace` : 1文字削除
133
150
  - `Up` / `Down` : 履歴移動
134
151
  - `Left` / `Right` : カーソル移動
135
- - `Tab` (`Ctrl-i`) : Ex 補完(`:` prefix 時、コマンド名/一部引数の文脈対応)
152
+ - `Tab` (`Ctrl-i`) : Ex 補完(`:` prefix 時、コマンド名/一部引数の文脈対応。`:git`/`:gh` サブコマンドも補完可能)
136
153
  - `Esc` : キャンセル
137
154
  - `Ctrl-c` : キャンセル
138
155
  - `Ctrl-z` : shell へ suspend(`fg` で復帰)
data/docs/command.md CHANGED
@@ -3,6 +3,7 @@
3
3
  ## 起動オプション(CLI, 現状)
4
4
 
5
5
  - `--help`, `--version`
6
+ - `-f`(follow mode: ファイルを開いて末尾から `tail -f` 相当の追従モードで起動)
6
7
  - `--clean`
7
8
  - `-d`(diff mode placeholder, 未実装メッセージ表示)
8
9
  - `-q {errorfile}`(quickfix startup placeholder。現状は未実装メッセージ表示)
@@ -70,7 +71,7 @@
70
71
  - 未保存変更がある場合は `!` なしで拒否
71
72
  - `:e!` は未保存変更を破棄して開き直す(undo/redo もクリア)
72
73
  - 大きいファイル(閾値以上)は段階読み込みになる場合がある
73
- - 読み込み中は status line に `[load/live]`
74
+ - 読み込み中は statusline に `[load/live]`
74
75
  - デフォルトでは先頭 `8MB` を先に表示して、残りを後から追加
75
76
  - 閾値: `RUVIM_ASYNC_FILE_THRESHOLD_BYTES`
76
77
  - 先読みサイズ: `RUVIM_ASYNC_FILE_PREFIX_BYTES`
@@ -114,9 +115,49 @@
114
115
  - current window の location list を開く / 閉じる
115
116
  - `:lnext` / `:ln`, `:lprev` / `:lp`
116
117
  - location list 項目を前後移動して該当位置へジャンプ
118
+ - `:grep pattern [path...]`
119
+ - 外部 grep(`grepprg`)を実行して quickfix list を作成
120
+ - シェル経由ではなく argv 配列で安全に実行(シェルインジェクション対策済み)
121
+ - ワイルドカード(`*.txt` 等)は Ruby の `Dir.glob` で展開
122
+ - `-Z`(restricted mode)では無効
123
+ - `:lgrep pattern [path...]`
124
+ - 外部 grep(`grepprg`)を実行して location list を作成
125
+ - `:grep` と同じセキュリティ仕様
126
+ - 一覧バッファ上で `Enter` : 選択項目へジャンプ(一覧ウィンドウから元の編集ウィンドウへ戻る)
117
127
  - 現状の制限:
118
- - `:grep`, `:make`, `:cfile`, `:lgrep` は未実装
119
- - 一覧バッファ上で `Enter` からのジャンプは未実装
128
+ - `:make`, `:cfile`, `:lfile` は未実装
129
+
130
+ ### `:git`
131
+
132
+ - 形式: `:git <subcommand> [args...]`
133
+ - Git 操作の統合インターフェース
134
+ - `-Z`(restricted mode)では無効
135
+ - サブコマンド:
136
+ - `blame` : 現在ファイルの git blame を表示(read-only バッファ)
137
+ - blame バッファ内: `p` 親コミットの blame / `P` 前の blame へ戻る / `c` コミット詳細
138
+ - `status` : git status を表示
139
+ - `diff` : git diff を表示
140
+ - `log` : git log を表示
141
+ - `branch` : ブランチ一覧を表示
142
+ - branch バッファ内: `Enter` で `:git checkout <branch>` をコマンドラインにプリセット(確認ステップあり)
143
+ - `checkout <branch>` : 指定ブランチへチェックアウト
144
+ - `commit` : コミットメッセージ入力バッファを開く
145
+ - `grep <pattern> [args...]` : git grep -n を実行、結果バッファで Enter ジャンプ
146
+ - その他の未知サブコマンド: `:!` と同様に alternate screen でシェル実行(例: `:git stash`, `:git rebase -i`)
147
+ - Git バッファは `Esc` / `Ctrl-c` で閉じる
148
+ - Tab 補完: `git help -a` の出力から全サブコマンドを補完候補として提示(セッション内キャッシュ)
149
+
150
+ ### `:gh`
151
+
152
+ - 形式: `:gh <subcommand> [args...]`
153
+ - GitHub 連携の統合インターフェース
154
+ - `-Z`(restricted mode)では無効
155
+ - サブコマンド:
156
+ - `link` : 現在ファイル/選択範囲の GitHub URL を生成してクリップボードにコピー
157
+ - `browse` : 現在ファイルの GitHub URL をブラウザで開く
158
+ - `pr` : 現在ブランチの PR ページをブラウザで開く
159
+ - その他の未知サブコマンド: `:!` と同様に alternate screen でシェル実行(例: `:gh issue list`)
160
+ - Tab 補完: `gh help` の出力からサブコマンドを補完候補として提示(セッション内キャッシュ)
120
161
 
121
162
  ### `:rich`
122
163
 
@@ -129,6 +170,36 @@
129
170
  - 列幅は画面に見えている行だけから計算(大規模ファイルでも高速)
130
171
  - `Esc` / `Ctrl-C` で Normal mode に戻る
131
172
 
173
+ ### `:filter`
174
+
175
+ - 形式: `:filter [/pattern/]`
176
+ - 現在の検索パターン(引数なし)または指定パターンにマッチする行だけを集めたフィルタバッファを作成
177
+ - フィルタバッファ上で再度 `:filter` や `g/` を使うと再帰的に絞り込める
178
+ - Enter で元バッファの該当行へジャンプ(フィルタバッファは閉じる)
179
+ - `:q` でフィルタバッファを閉じて前のバッファに戻る
180
+ - 元バッファの filetype を引き継ぐ(TSV なら `gr` でテーブル表示可能)
181
+ - Normal mode では `g/` キーバインドで実行可能
182
+
183
+ ### `:follow`
184
+
185
+ - 形式: `:follow`
186
+ - ファイルの follow mode をトグルする(`tail -f` 相当)
187
+ - follow 中はファイルへの追記がリアルタイムにバッファへ反映される
188
+ - カーソルが最終行にいる場合は末尾を自動追従(auto-scroll)
189
+ - カーソルが途中にある場合はスクロール位置を維持
190
+ - `G` で末尾に移動すれば追従が再開される
191
+ - `:follow` を再度実行すると停止
192
+ - `Ctrl-C`(ノーマルモード)で follow を停止
193
+ - statusline に `[follow]` と表示される(inotify 使用時は `[follow/i]`)
194
+ - ファイルが紐づいていないバッファではエラー
195
+ - 未保存の変更があるバッファでは follow 開始不可
196
+ - follow 中のバッファは変更不可(modifiable が false)
197
+ - ファイルが truncate された場合はメッセージを表示し監視を継続
198
+ - ファイルが削除された場合はメッセージを表示し、再作成を待って監視を再開
199
+ - follow 停止時、バッファ内容はそのまま保持(`:e!` で再読み込み可能)
200
+ - 監視方式: Linux では inotify(fiddle 経由)を優先し、使えない場合は polling にフォールバック
201
+ - polling は exponential backoff(0.1s → 3s)で CPU 負荷を抑える
202
+
132
203
  ### `:command`
133
204
 
134
205
  - 形式: `:command Name ex_body`
@@ -144,14 +215,50 @@
144
215
  - `stdout` / `stderr` に出力があった場合は `[Ruby Output]` 仮想バッファに表示(返り値も末尾に表示)
145
216
  - 利用可能: `ctx`, `editor`, `buffer`, `window`
146
217
 
147
- ### `:!`(shell 実行, 最小)
218
+ ### `:r` / `:read`(ファイル・コマンド出力の挿入)
219
+
220
+ - 形式: `:r <file>` または `:r !<command>`
221
+ - ファイルの内容またはシェルコマンドの stdout をカーソル行の下に挿入します
222
+ - 行番号指定可: `:3r file.txt`(3行目の下に挿入)
223
+ - `:r !command` で stderr が出た場合、最初の行をエラーメッセージとして表示します
224
+
225
+ ### `:w !`(バッファ内容をコマンドへパイプ)
226
+
227
+ - 形式: `:w !<command>` または `:'<,'>w !<command>`
228
+ - バッファ(または範囲指定された行)の内容をシェルコマンドの stdin に渡します
229
+ - 例: `:w !wc -l`(行数カウント)、`:'<,'>w !sort`(選択範囲をソート)
230
+
231
+ ### `:!`(shell 実行)
148
232
 
149
233
  - 形式: `:!<command>`
150
- - shell コマンドを同期実行します
151
- - `stdout` / `stderr` に出力があった場合は `[Shell Output]` 仮想バッファに表示(終了ステータスも表示)
152
- - 出力がない場合は `shell exit N` をステータス表示
234
+ - alternate screen を一時的に抜けて main screen 上で shell コマンドを同期実行します(Vim 互換)
235
+ - 実行後 "Press ENTER or type command to continue" を表示し、入力待ち後にエディタに復帰
236
+ - 完了後は `shell exit N` をステータス表示
153
237
  - `-Z`(restricted mode)では無効
154
238
 
239
+ ### `:run`(コマンド実行+結果バッファ)
240
+
241
+ - 形式: `:run [command]`
242
+ - シェルコマンドを実行し、出力を `[Shell Output]` バッファにストリーム表示(PTY 経由でリアルタイム)
243
+ - `[Shell Output]` バッファは read-only で、最大1つ(再利用される)
244
+ - stdout / stderr はマージされて表示
245
+ - コマンド中の `%` は現在のファイル名に展開される
246
+ - コマンド文字列はシェル引用符を保持(raw 解析)
247
+ - 変更のあるバッファは実行前に自動保存される
248
+ - 引数なしの場合:
249
+ - そのバッファで直前に実行したコマンドがあればそれを再実行
250
+ - なければ `runprg` オプションの値を使用
251
+ - バッファごとに実行履歴(直前のコマンド)を保持
252
+ - 実行中のコマンドはステータスラインに表示(`[run] command`、完了後は `[run/exit N] command`)
253
+ - `Ctrl-C` で実行中のプロセスを停止
254
+ - `runprg` オプション: ファイルタイプごとのデフォルト実行コマンド
255
+ - Ruby: `ruby -w %`
256
+ - Python: `python3 %`
257
+ - C: `gcc -Wall -o /tmp/a.out % && /tmp/a.out`
258
+ - C++: `g++ -Wall -o /tmp/a.out % && /tmp/a.out`
259
+ - Scheme: `gosh %`
260
+ - JavaScript: `node %`
261
+
155
262
  ### `:ls` / `:buffers`
156
263
 
157
264
  - 形式: `:ls`
@@ -247,6 +354,16 @@
247
354
  - 各タブに含まれるウィンドウとバッファ名を表示
248
355
  - 現在のタブ / ウィンドウは `>` マーカーで示す
249
356
 
357
+ ### `:d` / `:delete`
358
+
359
+ - 形式: `:d [count]`, `:{range}d`
360
+ - 行を削除
361
+
362
+ ### `:y` / `:yank`
363
+
364
+ - 形式: `:y [count]`, `:{range}y`
365
+ - 行をヤンク
366
+
250
367
  ## 内部コマンド(主なもの)
251
368
 
252
369
  内部コマンドは主に key binding から使われ、`RuVim::CommandRegistry` に登録されます。
@@ -302,6 +419,33 @@
302
419
  - `editor.buffer_next`
303
420
  - `editor.buffer_prev`
304
421
  - `buffer.replace_char`
422
+ - `buffer.substitute_char`
423
+ - `buffer.swapcase_char`
424
+ - `buffer.join_lines`
425
+ - `buffer.indent_lines`
426
+ - `buffer.indent_motion`
427
+ - `buffer.visual_indent`
428
+ - `buffer.visual_select_text_object`
429
+ - `mode.visual_block`
430
+ - `cursor.page_up` / `cursor.page_down`
431
+ - `cursor.page_up.default` / `cursor.page_down.default`
432
+ - `cursor.page_up.half` / `cursor.page_down.half`
433
+ - `cursor.match_bracket`
434
+ - `window.scroll_up` / `window.scroll_down`
435
+ - `window.scroll_up.line` / `window.scroll_down.line`
436
+ - `window.cursor_line_top` / `window.cursor_line_center` / `window.cursor_line_bottom`
437
+ - `window.focus_or_split_left` / `window.focus_or_split_right` / `window.focus_or_split_up` / `window.focus_or_split_down`
438
+ - `normal.register_pending_start`
439
+ - `normal.operator_delete_start` / `normal.operator_yank_start` / `normal.operator_change_start` / `normal.operator_indent_start`
440
+ - `normal.replace_pending_start`
441
+ - `normal.find_char_forward_start` / `normal.find_char_backward_start` / `normal.find_till_forward_start` / `normal.find_till_backward_start`
442
+ - `normal.find_repeat` / `normal.find_repeat_reverse`
443
+ - `normal.change_repeat`
444
+ - `normal.macro_record_toggle` / `normal.macro_play_pending_start`
445
+ - `normal.mark_pending_start`
446
+ - `normal.jump_mark_linewise_pending_start` / `normal.jump_mark_exact_pending_start`
447
+ - `quickfix.next` / `quickfix.prev` / `quickfix.open`
448
+ - `stdin.stream_stop`
305
449
  - `ui.clear_message`
306
450
  - `rich.toggle`
307
451
 
@@ -316,12 +460,14 @@
316
460
  - `*` / `#` : カーソル下の単語検索(単語境界)
317
461
  - `g*` / `g#` : カーソル下の単語検索(部分一致)
318
462
  - `:vimgrep` / `:lvimgrep` : 検索結果を quickfix/location list に積む
463
+ - `:nohlsearch` (`:noh`) : 検索ハイライトを一時的にクリア(次の検索で自動復帰)
319
464
 
320
- ### `:%s/.../.../g`(最小実装)
465
+ ### `:%s/.../.../[flags]`
321
466
 
322
- - バッファ全体置換
467
+ - バッファ全体置換(レンジ指定対応: `:{range}s/.../.../`)
323
468
  - Ruby 正規表現 + Ruby 置換文字列を利用
324
- - `g` フラグ対応(全置換)
469
+ - 対応フラグ: `g`(全置換), `i`(大文字小文字無視), `I`(大文字小文字区別), `n`(置換せずマッチ数表示), `e`(エラー抑制)
470
+ - `c`(確認)フラグは未実装
325
471
 
326
472
  ## Operator-pending(Normal mode)
327
473
 
data/docs/config.md CHANGED
@@ -244,11 +244,11 @@ setglobal "tabstop=8"
244
244
  - Tab 入力/削除時の編集幅を指定します。
245
245
  - 現状は Insert mode の Tab 入力と、`expandtab` 時の空白 Backspace(最小)で使用します。
246
246
 
247
- - `autoindent` (`bool`, default `false`) [`DONE`]
247
+ - `autoindent` (`bool`, default `true`) [`DONE`]
248
248
  - 改行時に前行の先頭インデントを引き継ぎます。
249
249
  - Insert mode `Enter`、`o`/`O` に反映されます。
250
250
 
251
- - `smartindent` (`bool`, default `false`) [`PARTIAL`]
251
+ - `smartindent` (`bool`, default `true`) [`PARTIAL`]
252
252
  - 簡易な自動インデントを行います。
253
253
  - 現状は前行が `{` `[` `(` で終わる場合に `shiftwidth` 分の空白を追加します。
254
254
 
@@ -265,6 +265,14 @@ setglobal "tabstop=8"
265
265
  - ftplugin 読み込み対象、filetype-local keymap、簡易 syntax highlight の判定に使います。
266
266
  - 手動で変更した場合の ftplugin 再適用は現状未対応です。
267
267
 
268
+ ### Global options(検索・grep)
269
+
270
+ - `grepprg` (`string`, default `"grep -nH"`) [`DONE`]
271
+ - `:grep` / `:lgrep` が使用する外部コマンドを指定します。
272
+
273
+ - `grepformat` (`string`, default `"%f:%l:%m"`) [`DONE`]
274
+ - `:grep` / `:lgrep` の出力パース書式を指定します。
275
+
268
276
  ## 制限(現状)
269
277
 
270
278
  - Vim の option 全部は未実装(ごく一部のみ)
data/docs/done.md CHANGED
@@ -47,6 +47,7 @@
47
47
  - `docs/plugin.md`(拡張 / plugin 的な書き方)
48
48
  - block-based keymap DSL(`nmap/imap/map_global ... do |ctx, ...| end`)
49
49
  - plugin 向け `ctx.editor / ctx.buffer / ctx.window` API リファレンス(未確定 API 注記つき)
50
+ - `:run` コマンド(PTY ストリーミング、`runprg` オプション、バッファごと実行履歴、`%` ファイル名展開、auto-save、Ctrl-C 停止、status line にコマンド表示)
50
51
 
51
52
  ## Rich mode / 構造化データ表示
52
53
 
@@ -77,6 +78,7 @@
77
78
  - PageUp / PageDown 対応
78
79
  - `:q` の window/tab/app クローズ挙動を Vim 寄りに調整
79
80
  - hit-enter prompt(複数行メッセージ表示: `:ls`, `:args`, `:set`, `:command`)
81
+ - カーソル形状制御(DECSCUSR: Normal では非表示+セル反転、Insert ではバーカーソル)
80
82
 
81
83
  ## Unicode / 日本語 / 表示幅 / 座標系
82
84
 
@@ -99,7 +101,11 @@
99
101
  - CI / 開発体験(GitHub Actions, `rake docs:check`, `rake ci`)
100
102
  - シンタックスハイライト(最小)
101
103
  - Markdown シンタックスハイライト(`Lang::Markdown` に抽出、通常モード + Rich mode で共有)
104
+ - C 言語モード(`Lang::C`): シンタックスハイライト・スマートインデント・保存時 gcc チェック
105
+ - C++ 言語モード(`Lang::Cpp`): C の全機能 + C++ 固有キーワード・アクセス指定子インデント・保存時 g++ チェック
102
106
  - 補完基盤(Ex 補完 / Insert mode 補完)
107
+ - git/gh サブコマンドの Tab 補完(`git help -a` / `gh help` から動的取得、セッション内キャッシュ)
108
+ - 未知の `:git` / `:gh` サブコマンドのシェルフォールバック実行
103
109
 
104
110
  ## CLI オプション(Vim 風・実装済み / placeholder 含む)
105
111
 
@@ -124,6 +130,23 @@
124
130
  - `-q {errorfile}`(quickfix 読み込み起動)
125
131
  - `-S [session]`(session 読み込み)
126
132
 
133
+ ## セキュリティ修正
134
+
135
+ - `:grep` / `:lgrep` のシェルインジェクション対策(argv 配列実行 + `Dir.glob` 展開)
136
+ - Rich view レンダリング時のターミナルエスケープインジェクション対策(制御文字の無害化)
137
+ - Restricted mode (`-Z`) の網羅強化(`:grep`, `:lgrep`, `:git`, `:gh` を無効化)
138
+ - Git branch checkout の安全化(Enter で即時実行せずコマンドラインにプリセットして確認ステップを挟む)
139
+ - 非同期ファイルローダーの OOM 対策(`bulk_once` モード廃止、チャンク読み込み)
140
+ - 特殊ファイル(FIFO/デバイス/ソケット)の読み込み拒否(`Buffer.ensure_regular_file!` で統一ガード)
141
+
142
+ ## Stream クラス階層化 / StreamMixer リネーム
143
+
144
+ - `@loading_state` を Stream に統合し、非同期ファイルロード状態を `Stream::FileLoad` で管理
145
+ - Stream を基底クラス + 5 サブクラス(`Stdin`, `Run`, `Follow`, `FileLoad`, `Git`)に分離
146
+ - fire-on-new パターン: `new` 時点でスレッド/ウォッチャーを即時起動
147
+ - `stop_handler` をコンストラクタで渡す(`attr_reader`)
148
+ - `StreamHandler` を `StreamMixer` にリネーム(責務を明確化: イベントキューの合流・分配)
149
+
127
150
  ## ドキュメント整理 / 仕様整備
128
151
 
129
152
  - `docs/spec.md`, `docs/tutorial.md`, `docs/binding.md`, `docs/command.md`, `docs/config.md`, `docs/vim_diff.md` の継続更新