ruvim 0.4.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  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 +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -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
@@ -90,8 +90,9 @@
90
90
  - `gr` : Rich mode をトグル(TSV/CSV をテーブル整形表示、同一バッファ上で動作)
91
91
  - `g/` : 検索パターンにマッチする行だけを集めたフィルタバッファを作成(再帰フィルタ可、Enter で元行へジャンプ、`:q` で戻る)
92
92
  - `Ctrl-g` : Git コマンドモード(`:git ` がプリセットされたコマンドラインに入る)
93
- - `blame` → Git blame を開く / `status` → Git status / `diff` → Git diff / `log` → Git log
93
+ - `blame` → Git blame を開く / `status` → Git status / `diff` → Git diff / `log` → Git log / `branch` → ブランチ一覧
94
94
  - Blame バッファ内: `p` 親コミットの blame へ遷移 / `P` 前の blame へ戻る / `c` コミット詳細を表示
95
+ - Branch バッファ内: `Enter` で `:git checkout <branch>` をコマンドラインにプリセット(即時実行ではなく確認ステップあり)
95
96
  - `Ctrl-c` : stdin stream 停止(stdin バッファ表示時)
96
97
  - `Esc` : メッセージ/保留入力のクリア
97
98
  - `Ctrl-z` : shell へ suspend(`fg` で復帰)
@@ -148,7 +149,7 @@
148
149
  - `Backspace` : 1文字削除
149
150
  - `Up` / `Down` : 履歴移動
150
151
  - `Left` / `Right` : カーソル移動
151
- - `Tab` (`Ctrl-i`) : Ex 補完(`:` prefix 時、コマンド名/一部引数の文脈対応)
152
+ - `Tab` (`Ctrl-i`) : Ex 補完(`:` prefix 時、コマンド名/一部引数の文脈対応。`:git`/`:gh` サブコマンドも補完可能)
152
153
  - `Esc` : キャンセル
153
154
  - `Ctrl-c` : キャンセル
154
155
  - `Ctrl-z` : shell へ suspend(`fg` で復帰)
data/docs/command.md CHANGED
@@ -71,7 +71,7 @@
71
71
  - 未保存変更がある場合は `!` なしで拒否
72
72
  - `:e!` は未保存変更を破棄して開き直す(undo/redo もクリア)
73
73
  - 大きいファイル(閾値以上)は段階読み込みになる場合がある
74
- - 読み込み中は status line に `[load/live]`
74
+ - 読み込み中は statusline に `[load/live]`
75
75
  - デフォルトでは先頭 `8MB` を先に表示して、残りを後から追加
76
76
  - 閾値: `RUVIM_ASYNC_FILE_THRESHOLD_BYTES`
77
77
  - 先読みサイズ: `RUVIM_ASYNC_FILE_PREFIX_BYTES`
@@ -115,13 +115,49 @@
115
115
  - current window の location list を開く / 閉じる
116
116
  - `:lnext` / `:ln`, `:lprev` / `:lp`
117
117
  - location list 項目を前後移動して該当位置へジャンプ
118
- - `:grep /pattern/ [path...]`
118
+ - `:grep pattern [path...]`
119
119
  - 外部 grep(`grepprg`)を実行して quickfix list を作成
120
- - `:lgrep /pattern/ [path...]`
120
+ - シェル経由ではなく argv 配列で安全に実行(シェルインジェクション対策済み)
121
+ - ワイルドカード(`*.txt` 等)は Ruby の `Dir.glob` で展開
122
+ - `-Z`(restricted mode)では無効
123
+ - `:lgrep pattern [path...]`
121
124
  - 外部 grep(`grepprg`)を実行して location list を作成
125
+ - `:grep` と同じセキュリティ仕様
126
+ - 一覧バッファ上で `Enter` : 選択項目へジャンプ(一覧ウィンドウから元の編集ウィンドウへ戻る)
122
127
  - 現状の制限:
123
128
  - `:make`, `:cfile`, `:lfile` は未実装
124
- - 一覧バッファ上で `Enter` からのジャンプは未実装
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` の出力からサブコマンドを補完候補として提示(セッション内キャッシュ)
125
161
 
126
162
  ### `:rich`
127
163
 
@@ -154,7 +190,7 @@
154
190
  - `G` で末尾に移動すれば追従が再開される
155
191
  - `:follow` を再度実行すると停止
156
192
  - `Ctrl-C`(ノーマルモード)で follow を停止
157
- - ステータスラインに `[follow]` と表示される(inotify 使用時は `[follow/i]`)
193
+ - statusline `[follow]` と表示される(inotify 使用時は `[follow/i]`)
158
194
  - ファイルが紐づいていないバッファではエラー
159
195
  - 未保存の変更があるバッファでは follow 開始不可
160
196
  - follow 中のバッファは変更不可(modifiable が false)
@@ -179,14 +215,50 @@
179
215
  - `stdout` / `stderr` に出力があった場合は `[Ruby Output]` 仮想バッファに表示(返り値も末尾に表示)
180
216
  - 利用可能: `ctx`, `editor`, `buffer`, `window`
181
217
 
182
- ### `:!`(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 実行)
183
232
 
184
233
  - 形式: `:!<command>`
185
- - shell コマンドを同期実行します
186
- - `stdout` / `stderr` に出力があった場合は `[Shell Output]` 仮想バッファに表示(終了ステータスも表示)
187
- - 出力がない場合は `shell exit N` をステータス表示
234
+ - alternate screen を一時的に抜けて main screen 上で shell コマンドを同期実行します(Vim 互換)
235
+ - 実行後 "Press ENTER or type command to continue" を表示し、入力待ち後にエディタに復帰
236
+ - 完了後は `shell exit N` をステータス表示
188
237
  - `-Z`(restricted mode)では無効
189
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
+
190
262
  ### `:ls` / `:buffers`
191
263
 
192
264
  - 形式: `:ls`
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` の継続更新