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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: affafdf81175fc15f4d0f0053982743caa863aa371053095bfdd24160aee857d
4
- data.tar.gz: 9abd61f0ba641b3f7dc429c4104f2e5d09243ebeb821baa8ad7521c6b4a3eca7
3
+ metadata.gz: ab74dcac81f3fb559503edb3536f762424d68aa072d623dfd8a8ba4762bf4b81
4
+ data.tar.gz: 4444a1c49d93e791326c70ca0c93d1b092ccaec74fa689ebdf3b5981af2ec751
5
5
  SHA512:
6
- metadata.gz: 7a5dc6394f0905831d384fb66d88992d20691b58347a2c1673179f0036585f82aa54c6681f8bb79dd9c0e3ffa77389cb5e7a62d7eec27571be0b7f79529a58fb
7
- data.tar.gz: 203a49cce3d46575fa743c5635874af2e320ddd225fe973d74a82851b1bfb6112e379e084e1e8afa262c5eac3d596c5c7b23ad572e77f8bf6ed5d2296e9fe992
6
+ metadata.gz: 91d7d472ab5fbb7cca606bef15048bfd3df43a9950c56fc8331d2dfb7f35316c5857cf16e8eca4b7dd67de022d129b5a2acd79e60309fe1aaf84db33557c2e22
7
+ data.tar.gz: e45f327ed892a34f4ea8582a3d65844286ffb312902e5496f4bb4166327251a3e284c3dd8143a873b50962ea13dfd038fc3983e55d0212dc5e5b1e8edf7b4dfb
data/AGENTS.md CHANGED
@@ -27,7 +27,16 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
27
27
 
28
28
  | File | Description |
29
29
  |------|-------------|
30
- | `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 |
31
40
  | `editor.rb` | Editor state: buffers, windows, options, registers, marks, modes |
32
41
  | `buffer.rb` | Text buffer (lines, file I/O, encoding) |
33
42
  | `window.rb` | View of a buffer (cursor, scroll, grapheme-aware movement) |
@@ -47,12 +56,32 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
47
56
  | `text_metrics.rb` | Grapheme-aware text measurement and navigation |
48
57
  | `keyword_chars.rb` | Word character definition (iskeyword) |
49
58
  | `highlighter.rb` | Syntax highlighting dispatcher (delegates to lang modules) |
59
+ | `lang/registry.rb` | Central lang registry (filetype detection, module/runprg lookup) |
50
60
  | `lang/base.rb` | Default lang module (no-op fallback for indent/dedent) |
51
61
  | `lang/markdown.rb` | Markdown parsing, detection helpers, and syntax highlight colors |
52
62
  | `lang/ruby.rb` | Ruby syntax highlighting via Prism lexer; auto-indent calculation |
53
63
  | `lang/json.rb` | JSON syntax highlighting via regex; auto-indent |
54
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 |
55
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) |
56
85
  | `lang/tsv.rb` | TSV detection and RichView renderer registration |
57
86
  | `lang/csv.rb` | CSV detection and RichView renderer registration |
58
87
  | `git/blame.rb` | Git blame: parser, runner, command handlers |
@@ -61,9 +90,12 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
61
90
  | `git/log.rb` | Git log: runner, command handlers |
62
91
  | `git/branch.rb` | Git branch: listing, checkout, command handlers |
63
92
  | `git/commit.rb` | Git commit: message buffer, execute, command handlers |
64
- | `git/handler.rb` | Git module (repo_root), dispatcher, close, shared helpers |
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 |
65
96
  | `file_watcher.rb` | File change monitoring (inotify with fiddle fallback to polling) |
66
97
  | `clipboard.rb` | System clipboard access (xclip, pbpaste, etc.) |
98
+ | `browser.rb` | URL open (open/xdg-open/wslview/PowerShell) |
67
99
  | `context.rb` | Command handler context (editor, window, buffer, invocation) |
68
100
  | `command_invocation.rb` | Single command invocation (id, argv, count, bang) |
69
101
  | `rich_view.rb` | Rich view mode (TSV/CSV/Markdown rendering) |
@@ -72,10 +104,27 @@ CLI (exe/ruvim) → CLI.parse() → App.new() → App.run_ui_loop()
72
104
  | `rich_view/json_renderer.rb` | JSON pretty-print into virtual buffer |
73
105
  | `rich_view/jsonl_renderer.rb` | JSONL per-line pretty-print into virtual buffer |
74
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 |
122
+
75
123
  ### Tests (test/)
76
124
 
77
- - 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`, `command_line_test`
78
- - 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`, `follow_test`, `git_blame_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`
79
128
  - Helper: `test_helper.rb` (fresh_editor, Minitest)
80
129
 
81
130
  ### Docs (docs/)
data/README.md CHANGED
@@ -9,6 +9,7 @@ Vim の操作感をベースに、Ruby ならではの、もしくは ko1 が欲
9
9
  - **Rich View (`gr`)** — TSV / CSV / Markdown をテーブル整形して閲覧。CJK 幅を考慮したカラム揃え
10
10
  - **`g/` 検索フィルタ** — 検索にマッチする行だけを集めたバッファを作成。再帰的に絞り込み可能。ログ解析に便利
11
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` でブラウザで開く
12
13
  - Ruby related:
13
14
  - **Ruby DSL 設定** — `~/.config/ruvim/init.rb` に Ruby で `nmap`, `set`, `command` を記述。Vim script 不要
14
15
  - **Ruby 正規表現** — 検索・置換は Ruby `Regexp`。Ruby ユーザーにそのまま馴染む
@@ -17,9 +18,11 @@ Vim の操作感をベースに、Ruby ならではの、もしくは ko1 が欲
17
18
  ## 概要
18
19
 
19
20
  - raw mode + ANSI 描画
20
- - Normal / Insert / Command-line / Visual
21
- - Ex コマンド(`:w`, `:q`, `:e`, `:help`, `:set` など)
22
- - 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`)
23
26
  - Ruby DSL 設定(XDG)
24
27
 
25
28
  ## 起動
@@ -68,6 +71,8 @@ ruvim path/to/file.txt
68
71
  - 複数ファイルを垂直 split で開く(最小実装)
69
72
  - `ruvim -p a.rb b.rb`
70
73
  - 複数ファイルを tab で開く(最小実装)
74
+ - `ruvim -f log.txt`
75
+ - follow mode(`tail -f` 相当)で起動
71
76
  - `ruvim -d file.txt`
72
77
  - diff mode placeholder(現状は未実装メッセージのみ)
73
78
  - `ruvim -q errors.log`
@@ -83,13 +88,17 @@ ruby -Ilib exe/ruvim
83
88
 
84
89
  ## 主な操作(抜粋)
85
90
 
86
- - 移動: `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`, `%`
87
92
  - 挿入: `i`, `a`, `A`, `I`, `o`, `O`
88
93
  - 編集: `x`, `dd`, `d{motion}`, `c{motion}`, `yy`, `yw`, `p`, `P`, `r<char>`
94
+ - text object: `iw`, `aw`, `i"`, `a"`, `i)`, `a)`, `ip`, `ap` など
89
95
  - 検索: `/`, `?`, `n`, `N`, `*`, `#`, `g*`, `g#`
90
- - Visual: `v`, `V`, `y`, `d`
96
+ - Visual: `v`, `V`, `Ctrl-v`, `y`, `d`
91
97
  - Undo/Redo: `u`, `Ctrl-r`
92
- - 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`(バッファをパイプ)
93
102
 
94
103
  詳しくは `docs/tutorial.md` を参照してください。
95
104
 
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."