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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c463658b0badbad140842446b82818bb797930372d4ccb8ff5e4275fbe627904
4
- data.tar.gz: 92b3c9f004f8b075f603d4b8f2e4c48255ac0ef3eea768dd3e290495ab661033
3
+ metadata.gz: ab74dcac81f3fb559503edb3536f762424d68aa072d623dfd8a8ba4762bf4b81
4
+ data.tar.gz: 4444a1c49d93e791326c70ca0c93d1b092ccaec74fa689ebdf3b5981af2ec751
5
5
  SHA512:
6
- metadata.gz: 46ec215ad18a1e72a62d54e0a95b24d87b7ee5b9093300a4597c8d96754ef4a1ce30f964101e1f2cd3dda8ecefcc74d7565602bb80f0eba6ce28cd2fb835a9ad
7
- data.tar.gz: 49827d053ac4b9c827b0cbe86ebb00ff8a6609af4998ef5060a2407d0022f33b47327c11eaef4efc75aca03e0ddc07660a8bf12214db28e8c98f58c83c705b60
6
+ metadata.gz: 91d7d472ab5fbb7cca606bef15048bfd3df43a9950c56fc8331d2dfb7f35316c5857cf16e8eca4b7dd67de022d129b5a2acd79e60309fe1aaf84db33557c2e22
7
+ data.tar.gz: e45f327ed892a34f4ea8582a3d65844286ffb312902e5496f4bb4166327251a3e284c3dd8143a873b50962ea13dfd038fc3983e55d0212dc5e5b1e8edf7b4dfb
data/AGENTS.md CHANGED
@@ -9,6 +9,7 @@ This project is RuVim, a Vim-like editor written in Ruby. Always run the full te
9
9
  * catch up changes
10
10
  * move completed tasks to done.md
11
11
  * update CLAUDE.md Source Tree section if files were added, removed, or renamed
12
+ * note that CLAUDE.md is a symbolic link to AGENTS.md. Commit AGENTS.md if modify CLAUDE.md
12
13
  * commit it
13
14
  * After committing, show the commit message
14
15
 
@@ -26,7 +27,16 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
26
27
 
27
28
  | File | Description |
28
29
  |------|-------------|
29
- | `app.rb` | Main application loop, input handling, startup |
30
+ | `app.rb` | Main application: initialization, run loop, config, startup |
31
+ | `key_handler.rb` | Key input dispatch, mode handling, pending states, insert editing |
32
+ | `completion_manager.rb` | Command-line/insert completion, history, incsearch preview |
33
+ | `stream_mixer.rb` | Stream coordinator: event queue, drain, editor integration |
34
+ | `stream.rb` | Stream base class (state, live?, status, stop!) |
35
+ | `stream/stdin.rb` | Stream::Stdin — reads from stdin pipe |
36
+ | `stream/run.rb` | Stream::Run — PTY command execution |
37
+ | `stream/follow.rb` | Stream::Follow — file watcher (inotify/polling) |
38
+ | `stream/file_load.rb` | Stream::FileLoad — async large file loading |
39
+ | `stream/git.rb` | Stream::Git — git command output via IO.popen |
30
40
  | `editor.rb` | Editor state: buffers, windows, options, registers, marks, modes |
31
41
  | `buffer.rb` | Text buffer (lines, file I/O, encoding) |
32
42
  | `window.rb` | View of a buffer (cursor, scroll, grapheme-aware movement) |
@@ -46,24 +56,75 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
46
56
  | `text_metrics.rb` | Grapheme-aware text measurement and navigation |
47
57
  | `keyword_chars.rb` | Word character definition (iskeyword) |
48
58
  | `highlighter.rb` | Syntax highlighting dispatcher (delegates to lang modules) |
59
+ | `lang/registry.rb` | Central lang registry (filetype detection, module/runprg lookup) |
49
60
  | `lang/base.rb` | Default lang module (no-op fallback for indent/dedent) |
50
61
  | `lang/markdown.rb` | Markdown parsing, detection helpers, and syntax highlight colors |
51
62
  | `lang/ruby.rb` | Ruby syntax highlighting via Prism lexer; auto-indent calculation |
52
- | `lang/json.rb` | JSON syntax highlighting via regex |
63
+ | `lang/json.rb` | JSON syntax highlighting via regex; auto-indent |
53
64
  | `lang/scheme.rb` | Scheme syntax highlighting via regex |
65
+ | `lang/c.rb` | C syntax highlighting, smart indent, on_save gcc check |
66
+ | `lang/cpp.rb` | C++ syntax highlighting (extends C), access specifier indent, on_save g++ check |
67
+ | `lang/diff.rb` | Diff syntax highlighting (add/delete/hunk/header colors) |
68
+ | `lang/yaml.rb` | YAML syntax highlighting, auto-indent |
69
+ | `lang/sh.rb` | Shell/Bash syntax highlighting, auto-indent |
70
+ | `lang/python.rb` | Python syntax highlighting (builtins, decorators), auto-indent |
71
+ | `lang/javascript.rb` | JavaScript syntax highlighting, auto-indent |
72
+ | `lang/typescript.rb` | TypeScript syntax highlighting (extends JS), auto-indent |
73
+ | `lang/html.rb` | HTML syntax highlighting (tags, attributes, entities) |
74
+ | `lang/toml.rb` | TOML syntax highlighting (tables, keys, datetime) |
75
+ | `lang/go.rb` | Go syntax highlighting, auto-indent |
76
+ | `lang/rust.rb` | Rust syntax highlighting (lifetimes, macros, attributes), auto-indent |
77
+ | `lang/makefile.rb` | Makefile syntax highlighting (targets, variables, directives) |
78
+ | `lang/dockerfile.rb` | Dockerfile syntax highlighting (instructions, variables) |
79
+ | `lang/sql.rb` | SQL syntax highlighting (case-insensitive keywords) |
80
+ | `lang/elixir.rb` | Elixir syntax highlighting (atoms, modules, sigils), auto-indent |
81
+ | `lang/perl.rb` | Perl syntax highlighting (sigils, POD), auto-indent |
82
+ | `lang/lua.rb` | Lua syntax highlighting (builtins), auto-indent |
83
+ | `lang/ocaml.rb` | OCaml syntax highlighting (type vars, block comments), auto-indent |
84
+ | `lang/erb.rb` | ERB syntax highlighting (HTML + Ruby delimiters/comments) |
54
85
  | `lang/tsv.rb` | TSV detection and RichView renderer registration |
55
86
  | `lang/csv.rb` | CSV detection and RichView renderer registration |
87
+ | `git/blame.rb` | Git blame: parser, runner, command handlers |
88
+ | `git/status.rb` | Git status: runner, filename parser, command handlers |
89
+ | `git/diff.rb` | Git diff: runner, command handlers |
90
+ | `git/log.rb` | Git log: runner, command handlers |
91
+ | `git/branch.rb` | Git branch: listing, checkout, command handlers |
92
+ | `git/commit.rb` | Git commit: message buffer, execute, command handlers |
93
+ | `git/grep.rb` | Git grep: search, location parser, command handlers |
94
+ | `git/handler.rb` | Git/GitHub module (repo_root), dispatcher, close, shared helpers |
95
+ | `gh/link.rb` | GitHub link: URL generation, OSC 52 clipboard, command handlers |
96
+ | `file_watcher.rb` | File change monitoring (inotify with fiddle fallback to polling) |
56
97
  | `clipboard.rb` | System clipboard access (xclip, pbpaste, etc.) |
98
+ | `browser.rb` | URL open (open/xdg-open/wslview/PowerShell) |
57
99
  | `context.rb` | Command handler context (editor, window, buffer, invocation) |
58
100
  | `command_invocation.rb` | Single command invocation (id, argv, count, bang) |
59
101
  | `rich_view.rb` | Rich view mode (TSV/CSV/Markdown rendering) |
60
102
  | `rich_view/table_renderer.rb` | Table formatting with display-width-aware column alignment |
61
103
  | `rich_view/markdown_renderer.rb` | Markdown rendering (headings, inline, tables, code blocks, HR) |
104
+ | `rich_view/json_renderer.rb` | JSON pretty-print into virtual buffer |
105
+ | `rich_view/jsonl_renderer.rb` | JSONL per-line pretty-print into virtual buffer |
106
+
107
+ ### C Extension (ext/ruvim/)
108
+
109
+ | File | Description |
110
+ |------|-------------|
111
+ | `extconf.rb` | Build configuration for C extension |
112
+ | `ruvim_ext.c` | C implementation of DisplayWidth and TextMetrics hot paths |
113
+
114
+ ### Benchmarks (benchmark/)
115
+
116
+ | File | Description |
117
+ |------|-------------|
118
+ | `hotspots.rb` | Profile individual function hotspots (pure Ruby) |
119
+ | `cext_compare.rb` | Compare Ruby vs C extension performance |
120
+ | `chunked_load.rb` | Compare file loading strategies |
121
+ | `file_load.rb` | Profile large file loading bottlenecks |
62
122
 
63
123
  ### Tests (test/)
64
124
 
65
- - Unit: `buffer_test`, `window_test`, `editor_test`, `screen_test`, `display_width_test`, `text_metrics_test`, `keymap_manager_test`, `highlighter_test`, `dispatcher_test`, `config_*_test`, `indent_test`
66
- - Integration: `app_scenario_test`, `app_motion_test`, `app_text_object_test`, `app_register_test`, `app_dot_repeat_test`, `app_completion_test`, `app_unicode_behavior_test`, `render_snapshot_test`, `on_save_hook_test`
125
+ - Unit: `buffer_test`, `window_test`, `editor_test`, `screen_test`, `display_width_test`, `text_metrics_test`, `keymap_manager_test`, `highlighter_test`, `dispatcher_test`, `config_*_test`, `indent_test`, `file_watcher_test`, `clipboard_test`, `browser_test`, `command_line_test`, `keyword_chars_test`, `ex_command_registry_test`, `command_invocation_test`
126
+ - Lang: `lang_test` (syntax highlighting & filetype detection for all 23 languages)
127
+ - Integration: `app_scenario_test`, `app_motion_test`, `app_text_object_test`, `app_register_test`, `app_dot_repeat_test`, `app_completion_test`, `app_unicode_behavior_test`, `app_command_test`, `app_ex_command_test`, `render_snapshot_test`, `on_save_hook_test`, `follow_test`, `git_blame_test`, `git_grep_test`, `gh_link_test`, `run_command_test`, `stream_test`
67
128
  - Helper: `test_helper.rb` (fresh_editor, Minitest)
68
129
 
69
130
  ### Docs (docs/)
@@ -74,11 +135,11 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
74
135
 
75
136
  lumitrace is a tool that records runtime values of each Ruby expression.
76
137
  When a test fails, read `lumitrace help` first, then use it.
77
- Basic: `lumitrace -t exec rake test`
138
+ Basic: `lumitrace -j exec rake test`
139
+ This also provides coverage information for the test run.
78
140
 
79
141
  When fixing bugs, do NOT assume the first fix attempt is correct. After applying a fix, re-read the relevant code paths to verify the fix addresses the actual root cause, not a symptom. If the user says 'it hasn't changed' or equivalent, start fresh analysis from the failing behavior.
80
142
 
81
143
  ## misc
82
144
 
83
- The user communicates in both English and Japanese. Respond in the same language the user uses. When the user gives feedback like 変わってないですよ ('it hasn't changed'), treat it as a bug report requiring re-analysis.
84
-
145
+ The user communicates in both English and Japanese. Respond in the same language the user uses. When the user gives feedback like 変わってないですよ ('it hasn't changed'), treat it as a bug report requiring re-analysis.
data/README.md CHANGED
@@ -2,10 +2,27 @@
2
2
 
3
3
  Ruby で実装した Vim ライクなターミナルエディタです。
4
4
 
5
+ Vim の操作感をベースに、Ruby ならではの、もしくは ko1 が欲しい拡張性と独自機能を加えています。
6
+
7
+ ## Vim にない独自機能
8
+
9
+ - **Rich View (`gr`)** — TSV / CSV / Markdown をテーブル整形して閲覧。CJK 幅を考慮したカラム揃え
10
+ - **`g/` 検索フィルタ** — 検索にマッチする行だけを集めたバッファを作成。再帰的に絞り込み可能。ログ解析に便利
11
+ - **Follow mode (`-f` / `:follow`)** — `tail -f` 相当のファイル追従。inotify 対応
12
+ - **Git / GitHub 統合** — `:git blame`, `:git status`, `:git diff`, `:git log`, `:git branch`, `:git commit`, `:git grep` をエディタ内で実行。`:gh link` で GitHub URL をクリップボードにコピー、`:gh browse` でブラウザで開く
13
+ - Ruby related:
14
+ - **Ruby DSL 設定** — `~/.config/ruvim/init.rb` に Ruby で `nmap`, `set`, `command` を記述。Vim script 不要
15
+ - **Ruby 正規表現** — 検索・置換は Ruby `Regexp`。Ruby ユーザーにそのまま馴染む
16
+ - **`:ruby` eval** — 実行中に任意の Ruby コードを評価。`ctx.editor` / `ctx.buffer` API でエディタを操作
17
+
18
+ ## 概要
19
+
5
20
  - raw mode + ANSI 描画
6
- - Normal / Insert / Command-line / Visual
7
- - Ex コマンド(`:w`, `:q`, `:e`, `:help`, `:set` など)
8
- - split / vsplit / tab(最小実装)
21
+ - Normal / Insert / Command-line / Visual(char / line / block)
22
+ - Ex コマンド(`:w`, `:q`, `:e`, `:help`, `:set`, `:git`, `:gh` など)
23
+ - split / vsplit / tab
24
+ - quickfix / location list(`:vimgrep`, `:grep`, `:copen` など)
25
+ - シェル連携(`:!cmd`, `:r !cmd`, `:w !cmd`)
9
26
  - Ruby DSL 設定(XDG)
10
27
 
11
28
  ## 起動
@@ -54,6 +71,8 @@ ruvim path/to/file.txt
54
71
  - 複数ファイルを垂直 split で開く(最小実装)
55
72
  - `ruvim -p a.rb b.rb`
56
73
  - 複数ファイルを tab で開く(最小実装)
74
+ - `ruvim -f log.txt`
75
+ - follow mode(`tail -f` 相当)で起動
57
76
  - `ruvim -d file.txt`
58
77
  - diff mode placeholder(現状は未実装メッセージのみ)
59
78
  - `ruvim -q errors.log`
@@ -69,13 +88,17 @@ ruby -Ilib exe/ruvim
69
88
 
70
89
  ## 主な操作(抜粋)
71
90
 
72
- - 移動: `h j k l`, `w b e`, `0 ^ $`, `gg`, `G`
91
+ - 移動: `h j k l`, `w b e`, `0 ^ $`, `gg`, `G`, `f/F/t/T`, `%`
73
92
  - 挿入: `i`, `a`, `A`, `I`, `o`, `O`
74
93
  - 編集: `x`, `dd`, `d{motion}`, `c{motion}`, `yy`, `yw`, `p`, `P`, `r<char>`
94
+ - text object: `iw`, `aw`, `i"`, `a"`, `i)`, `a)`, `ip`, `ap` など
75
95
  - 検索: `/`, `?`, `n`, `N`, `*`, `#`, `g*`, `g#`
76
- - Visual: `v`, `V`, `y`, `d`
96
+ - Visual: `v`, `V`, `Ctrl-v`, `y`, `d`
77
97
  - Undo/Redo: `u`, `Ctrl-r`
78
- - Ex: `:w`, `:q`, `:e`, `:help`, `:commands`, `:set`
98
+ - マクロ: `q{reg}`, `@{reg}`, `@@`
99
+ - Ex: `:w`, `:q`, `:e`, `:help`, `:set`, `:git`, `:gh`
100
+ - Git: `Ctrl-g` で `:git` プリセット入力。blame / status / diff / log / branch / commit / grep
101
+ - シェル: `:!cmd`, `:r !cmd`(出力を挿入), `:w !cmd`(バッファをパイプ)
79
102
 
80
103
  詳しくは `docs/tutorial.md` を参照してください。
81
104
 
@@ -132,4 +155,4 @@ lint / format 方針(現状):
132
155
  - Vim 完全互換ではありません
133
156
  - 正規表現は Vim regex ではなく Ruby `Regexp` を使います
134
157
  - 文字幅 / Unicode は改善済みですが、完全互換ではありません
135
- - 複数ファイル引数や一部 Vim CLI オプションは未実装です
158
+ - 複数ファイル引数や一部 Vim CLI オプションは未実装です
data/Rakefile CHANGED
@@ -2,11 +2,18 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "rake/extensiontask"
6
+
7
+ Rake::ExtensionTask.new("ruvim_ext") do |ext|
8
+ ext.lib_dir = "lib/ruvim"
9
+ ext.ext_dir = "ext/ruvim"
10
+ end
5
11
 
6
12
  Rake::TestTask.new(:test) do |t|
7
13
  t.libs << "lib" << "test"
8
14
  t.test_files = FileList["test/*_test.rb"]
9
15
  end
16
+ task test: :compile
10
17
 
11
18
  namespace :docs do
12
19
  task :check do
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Compare Ruby vs C extension for DisplayWidth and TextMetrics
5
+ #
6
+ # Usage: ruby benchmark/cext_compare.rb
7
+
8
+ require "benchmark"
9
+
10
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
11
+ $LOAD_PATH.unshift File.expand_path("../ext/ruvim", __dir__)
12
+
13
+ require "ruvim/display_width"
14
+ require "ruvim/text_metrics"
15
+ require "ruvim_ext"
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Test data
19
+ # ---------------------------------------------------------------------------
20
+ ASCII_LINE = ' def foo(bar, baz) = bar + baz # comment with some padding text here!!!' * 2
21
+ CJK_LINE = "日本語テキスト 漢字カタカナ テスト用の行 全角文字を含む行" * 2
22
+ MIXED_LINE = "Hello 世界! def foo(x) = x + 1 # コメント 🚀 emoji test"
23
+ EMOJI_LINE = "🎉🔥💡🚀✨🎊🌟💎🏆🎯" * 5
24
+ TAB_LINE = "\t\tif (x > 0) {\n\t\t\treturn x;\n\t\t}"
25
+
26
+ LINES = {
27
+ "ASCII (140c)" => ASCII_LINE,
28
+ "CJK (56c)" => CJK_LINE,
29
+ "Mixed" => MIXED_LINE,
30
+ "Emoji (50c)" => EMOJI_LINE,
31
+ "Tabs" => TAB_LINE,
32
+ }.freeze
33
+
34
+ SCREEN_WIDTH = 120
35
+ SCREEN_ROWS = 50
36
+
37
+ N = 10_000
38
+ N_RENDER = 2_000
39
+
40
+ puts "DisplayWidth + TextMetrics: Ruby vs C Extension"
41
+ puts "=" * 70
42
+ puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})"
43
+ puts "Iterations: #{N} (per-call), #{N_RENDER} (render)"
44
+ puts
45
+
46
+ # ===================================================================
47
+ # Correctness checks
48
+ # ===================================================================
49
+ puts "--- Correctness check ---"
50
+ ok = true
51
+
52
+ # DisplayWidth
53
+ LINES.each do |label, line|
54
+ rw = RuVim::DisplayWidth.display_width(line)
55
+ cw = RuVim::DisplayWidthExt.display_width(line)
56
+ status = rw == cw ? "OK" : "MISMATCH"
57
+ puts " display_width %-15s Ruby=%3d C=%3d %s" % [label, rw, cw, status]
58
+ ok = false if rw != cw
59
+ end
60
+
61
+ # clip_cells_for_width
62
+ LINES.each do |label, line|
63
+ r_cells, r_col = RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH)
64
+ c_cells, c_col = RuVim::TextMetricsExt.clip_cells_for_width(line, SCREEN_WIDTH)
65
+ col_ok = r_col == c_col
66
+ count_ok = r_cells.size == c_cells.size
67
+ glyphs_ok = r_cells.map(&:glyph) == c_cells.map(&:glyph)
68
+ src_ok = r_cells.map(&:source_col) == c_cells.map(&:source_col)
69
+ widths_ok = r_cells.map(&:display_width) == c_cells.map(&:display_width)
70
+ all_ok = col_ok && count_ok && glyphs_ok && src_ok && widths_ok
71
+ status = all_ok ? "OK" : "MISMATCH"
72
+ unless all_ok
73
+ puts " clip_cells %-15s %s (col:%s cnt:%s glyph:%s src:%s w:%s)" %
74
+ [label, status, col_ok, count_ok, glyphs_ok, src_ok, widths_ok]
75
+ ok = false
76
+ else
77
+ puts " clip_cells %-15s %s (cells=%d, col=%d)" % [label, status, r_cells.size, r_col]
78
+ end
79
+ end
80
+
81
+ # char_index_for_screen_col
82
+ LINES.each do |label, line|
83
+ target = RuVim::DisplayWidth.display_width(line) / 2
84
+ ri = RuVim::TextMetrics.char_index_for_screen_col(line, target)
85
+ ci = RuVim::TextMetricsExt.char_index_for_screen_col(line, target)
86
+ status = ri == ci ? "OK" : "MISMATCH"
87
+ puts " char_idx_for_sc %-11s Ruby=%3d C=%3d %s" % [label, ri, ci, status]
88
+ ok = false if ri != ci
89
+ end
90
+
91
+ unless ok
92
+ puts "\n*** CORRECTNESS FAILURES — fix C extension before benchmarking ***"
93
+ exit 1
94
+ end
95
+ puts " All checks passed!"
96
+ puts
97
+
98
+ # ===================================================================
99
+ # 1. DisplayWidth benchmarks
100
+ # ===================================================================
101
+ puts "--- 1. cell_width ---"
102
+ test_chars = { "ASCII 'A'" => "A", "CJK '漢'" => "漢", "Emoji '🚀'" => "🚀" }
103
+ Benchmark.bm(25) do |x|
104
+ test_chars.each do |label, ch|
105
+ x.report("Ruby #{label}") { N.times { RuVim::DisplayWidth.cell_width(ch) } }
106
+ x.report("C #{label}") { N.times { RuVim::DisplayWidthExt.cell_width(ch) } }
107
+ end
108
+ end
109
+ puts
110
+
111
+ puts "--- 2. display_width ---"
112
+ Benchmark.bm(25) do |x|
113
+ LINES.each do |label, line|
114
+ x.report("Ruby #{label}") { N.times { RuVim::DisplayWidth.display_width(line) } }
115
+ x.report("C #{label}") { N.times { RuVim::DisplayWidthExt.display_width(line) } }
116
+ end
117
+ end
118
+ puts
119
+
120
+ # ===================================================================
121
+ # 2. TextMetrics benchmarks
122
+ # ===================================================================
123
+ puts "--- 3. clip_cells_for_width (Ruby TM vs C TM) ---"
124
+ Benchmark.bm(25) do |x|
125
+ LINES.each do |label, line|
126
+ x.report("Ruby #{label}") { N_RENDER.times { RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH) } }
127
+ x.report("C #{label}") { N_RENDER.times { RuVim::TextMetricsExt.clip_cells_for_width(line, SCREEN_WIDTH) } }
128
+ end
129
+ end
130
+ puts
131
+
132
+ puts "--- 4. char_index_for_screen_col (Ruby TM vs C TM) ---"
133
+ Benchmark.bm(25) do |x|
134
+ LINES.each do |label, line|
135
+ target = RuVim::DisplayWidth.display_width(line) / 2
136
+ x.report("Ruby #{label}") { N.times { RuVim::TextMetrics.char_index_for_screen_col(line, target) } }
137
+ x.report("C #{label}") { N.times { RuVim::TextMetricsExt.char_index_for_screen_col(line, target) } }
138
+ end
139
+ end
140
+ puts
141
+
142
+ # ===================================================================
143
+ # 3. Full screen simulation
144
+ # ===================================================================
145
+ puts "--- 5. Full screen render sim (#{SCREEN_ROWS} lines × #{SCREEN_WIDTH} cols) ---"
146
+ lines_80 = [ASCII_LINE, CJK_LINE, MIXED_LINE, ASCII_LINE, CJK_LINE].freeze
147
+ screen_lines = Array.new(SCREEN_ROWS) { |i| lines_80[i % lines_80.size] }
148
+
149
+ Benchmark.bm(25) do |x|
150
+ x.report("Ruby TM + Ruby DW") do
151
+ N_RENDER.times do
152
+ screen_lines.each { |line| RuVim::TextMetrics.clip_cells_for_width(line, SCREEN_WIDTH) }
153
+ end
154
+ end
155
+
156
+ x.report("C TM (full C)") do
157
+ N_RENDER.times do
158
+ screen_lines.each { |line| RuVim::TextMetricsExt.clip_cells_for_width(line, SCREEN_WIDTH) }
159
+ end
160
+ end
161
+ end
162
+ puts
163
+
164
+ puts "=" * 70
165
+ puts "Done."
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Benchmark different chunked file loading strategies
5
+ #
6
+ # Usage: ruby benchmark/chunked_load.rb [path]
7
+
8
+ require "benchmark"
9
+
10
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
11
+ require "ruvim/buffer"
12
+ require "ruvim/lang/registry"
13
+ require "ruvim/lang/base"
14
+
15
+ FILE = ARGV[0] || File.expand_path("../huge_file", __dir__)
16
+ abort "File not found: #{FILE}" unless File.exist?(FILE)
17
+
18
+ size_mb = File.size(FILE) / 1024.0 / 1024.0
19
+ puts "Chunked Load Benchmark"
20
+ puts "=" * 60
21
+ puts "Ruby: #{RUBY_VERSION}"
22
+ puts "File: #{FILE} (%.1f MB)" % size_mb
23
+ puts
24
+
25
+ CHUNK = 1 * 1024 * 1024
26
+ FLUSH = 4 * 1024 * 1024
27
+
28
+ # Baseline: sync read
29
+ puts "--- 0. Baseline: sync Buffer.from_file ---"
30
+ Benchmark.bm(30) do |x|
31
+ x.report("sync (binread + split)") do
32
+ data = RuVim::Buffer.decode_text(File.binread(FILE))
33
+ lines = RuVim::Buffer.split_lines(data)
34
+ lines.size
35
+ end
36
+ end
37
+ puts
38
+
39
+ # Strategy A: current async (concat per chunk)
40
+ puts "--- A. Current async (concat per 4MB chunk) ---"
41
+ Benchmark.bm(30) do |x|
42
+ x.report("concat per chunk") do
43
+ io = File.open(FILE, "rb")
44
+ lines = [""]
45
+ pending = "".b
46
+ begin
47
+ loop do
48
+ chunk = io.readpartial(CHUNK)
49
+ pending << chunk
50
+ next if pending.bytesize < FLUSH
51
+
52
+ last_nl = pending.rindex("\n".b)
53
+ if last_nl
54
+ send_bytes = pending[0..last_nl]
55
+ pending = pending[(last_nl + 1)..] || "".b
56
+ else
57
+ send_bytes = pending
58
+ pending = "".b
59
+ end
60
+ decoded = RuVim::Buffer.decode_text(send_bytes)
61
+ parts = decoded.split("\n", -1)
62
+ head = parts.shift || ""
63
+ lines[-1] = lines[-1] + head unless head.empty?
64
+ lines.concat(parts) unless parts.empty?
65
+ end
66
+ rescue EOFError
67
+ unless pending.empty?
68
+ decoded = RuVim::Buffer.decode_text(pending)
69
+ parts = decoded.split("\n", -1)
70
+ head = parts.shift || ""
71
+ lines[-1] = lines[-1] + head unless head.empty?
72
+ lines.concat(parts) unless parts.empty?
73
+ end
74
+ ensure
75
+ io.close
76
+ end
77
+ puts " lines: #{lines.size}"
78
+ end
79
+ end
80
+ puts
81
+
82
+ # Strategy B: larger flush size (32MB)
83
+ puts "--- B. Larger flush (32MB) ---"
84
+ FLUSH_LARGE = 32 * 1024 * 1024
85
+ Benchmark.bm(30) do |x|
86
+ x.report("32MB flush") do
87
+ io = File.open(FILE, "rb")
88
+ lines = [""]
89
+ pending = "".b
90
+ begin
91
+ loop do
92
+ chunk = io.readpartial(CHUNK)
93
+ pending << chunk
94
+ next if pending.bytesize < FLUSH_LARGE
95
+
96
+ last_nl = pending.rindex("\n".b)
97
+ if last_nl
98
+ send_bytes = pending[0..last_nl]
99
+ pending = pending[(last_nl + 1)..] || "".b
100
+ else
101
+ send_bytes = pending
102
+ pending = "".b
103
+ end
104
+ decoded = RuVim::Buffer.decode_text(send_bytes)
105
+ parts = decoded.split("\n", -1)
106
+ head = parts.shift || ""
107
+ lines[-1] = lines[-1] + head unless head.empty?
108
+ lines.concat(parts) unless parts.empty?
109
+ end
110
+ rescue EOFError
111
+ unless pending.empty?
112
+ decoded = RuVim::Buffer.decode_text(pending)
113
+ parts = decoded.split("\n", -1)
114
+ head = parts.shift || ""
115
+ lines[-1] = lines[-1] + head unless head.empty?
116
+ lines.concat(parts) unless parts.empty?
117
+ end
118
+ ensure
119
+ io.close
120
+ end
121
+ puts " lines: #{lines.size}"
122
+ end
123
+ end
124
+ puts
125
+
126
+ # Strategy C: collect sub-arrays, flatten once at end
127
+ puts "--- C. Collect sub-arrays, flatten at end ---"
128
+ Benchmark.bm(30) do |x|
129
+ x.report("collect + flatten") do
130
+ io = File.open(FILE, "rb")
131
+ segments = [] # array of arrays
132
+ carry = "" # partial line from previous chunk
133
+ pending = "".b
134
+ begin
135
+ loop do
136
+ chunk = io.readpartial(CHUNK)
137
+ pending << chunk
138
+ next if pending.bytesize < FLUSH
139
+
140
+ last_nl = pending.rindex("\n".b)
141
+ if last_nl
142
+ send_bytes = pending[0..last_nl]
143
+ pending = pending[(last_nl + 1)..] || "".b
144
+ else
145
+ send_bytes = pending
146
+ pending = "".b
147
+ end
148
+ decoded = RuVim::Buffer.decode_text(send_bytes)
149
+ parts = decoded.split("\n", -1)
150
+ # Merge carry into first element
151
+ unless carry.empty?
152
+ parts[0] = carry + (parts[0] || "")
153
+ carry = ""
154
+ end
155
+ # Last element is partial (no trailing newline guaranteed by rindex)
156
+ carry = parts.pop || ""
157
+ segments << parts unless parts.empty?
158
+ end
159
+ rescue EOFError
160
+ unless pending.empty?
161
+ decoded = RuVim::Buffer.decode_text(pending)
162
+ parts = decoded.split("\n", -1)
163
+ unless carry.empty?
164
+ parts[0] = carry + (parts[0] || "")
165
+ carry = ""
166
+ end
167
+ carry = parts.pop || ""
168
+ segments << parts unless parts.empty?
169
+ end
170
+ ensure
171
+ io.close
172
+ end
173
+ # Final flatten
174
+ lines = segments.flatten(1)
175
+ lines << carry unless carry.empty?
176
+ lines = [""] if lines.empty?
177
+ puts " lines: #{lines.size}"
178
+ end
179
+ end
180
+ puts
181
+
182
+ # Strategy D: single large read + split (like sync but via IO.read chunks)
183
+ puts "--- D. Read all into one string, split once ---"
184
+ Benchmark.bm(30) do |x|
185
+ x.report("IO.read full + split") do
186
+ data = "".b
187
+ io = File.open(FILE, "rb")
188
+ begin
189
+ loop { data << io.readpartial(CHUNK) }
190
+ rescue EOFError
191
+ ensure
192
+ io.close
193
+ end
194
+ decoded = RuVim::Buffer.decode_text(data)
195
+ lines = RuVim::Buffer.split_lines(decoded)
196
+ puts " lines: #{lines.size}"
197
+ end
198
+ end
199
+ puts
200
+
201
+ # Strategy E: larger read chunk (4MB) + larger flush (32MB) + collect
202
+ puts "--- E. 4MB read + 32MB flush + collect ---"
203
+ CHUNK_LARGE = 4 * 1024 * 1024
204
+ Benchmark.bm(30) do |x|
205
+ x.report("4MB read + 32MB flush") do
206
+ io = File.open(FILE, "rb")
207
+ segments = []
208
+ carry = ""
209
+ pending = "".b
210
+ begin
211
+ loop do
212
+ chunk = io.readpartial(CHUNK_LARGE)
213
+ pending << chunk
214
+ next if pending.bytesize < FLUSH_LARGE
215
+
216
+ last_nl = pending.rindex("\n".b)
217
+ if last_nl
218
+ send_bytes = pending[0..last_nl]
219
+ pending = pending[(last_nl + 1)..] || "".b
220
+ else
221
+ send_bytes = pending
222
+ pending = "".b
223
+ end
224
+ decoded = RuVim::Buffer.decode_text(send_bytes)
225
+ parts = decoded.split("\n", -1)
226
+ unless carry.empty?
227
+ parts[0] = carry + (parts[0] || "")
228
+ carry = ""
229
+ end
230
+ carry = parts.pop || ""
231
+ segments << parts unless parts.empty?
232
+ end
233
+ rescue EOFError
234
+ unless pending.empty?
235
+ decoded = RuVim::Buffer.decode_text(pending)
236
+ parts = decoded.split("\n", -1)
237
+ unless carry.empty?
238
+ parts[0] = carry + (parts[0] || "")
239
+ carry = ""
240
+ end
241
+ carry = parts.pop || ""
242
+ segments << parts unless parts.empty?
243
+ end
244
+ ensure
245
+ io.close
246
+ end
247
+ lines = segments.flatten(1)
248
+ lines << carry unless carry.empty?
249
+ lines = [""] if lines.empty?
250
+ puts " lines: #{lines.size}"
251
+ end
252
+ end
253
+ puts
254
+
255
+ puts "=" * 60
256
+ puts "Done."