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.
- checksums.yaml +4 -4
- data/AGENTS.md +53 -4
- data/README.md +15 -6
- data/Rakefile +7 -0
- data/benchmark/cext_compare.rb +165 -0
- data/benchmark/chunked_load.rb +256 -0
- data/benchmark/file_load.rb +140 -0
- data/benchmark/hotspots.rb +178 -0
- data/docs/binding.md +3 -2
- data/docs/command.md +81 -9
- data/docs/done.md +23 -0
- data/docs/spec.md +105 -19
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +9 -1
- data/docs/vim_diff.md +13 -0
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +217 -2778
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +39 -28
- data/lib/ruvim/command_invocation.rb +2 -2
- data/lib/ruvim/completion_manager.rb +708 -0
- data/lib/ruvim/dispatcher.rb +14 -8
- data/lib/ruvim/display_width.rb +91 -45
- data/lib/ruvim/editor.rb +64 -81
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +16 -6
- data/lib/ruvim/git/branch.rb +20 -5
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +42 -1
- data/lib/ruvim/global_commands.rb +175 -35
- data/lib/ruvim/highlighter.rb +4 -13
- data/lib/ruvim/key_handler.rb +1510 -0
- data/lib/ruvim/keymap_manager.rb +7 -7
- data/lib/ruvim/lang/base.rb +5 -0
- data/lib/ruvim/lang/c.rb +116 -0
- data/lib/ruvim/lang/cpp.rb +107 -0
- data/lib/ruvim/lang/csv.rb +4 -1
- data/lib/ruvim/lang/diff.rb +2 -0
- data/lib/ruvim/lang/dockerfile.rb +36 -0
- data/lib/ruvim/lang/elixir.rb +85 -0
- data/lib/ruvim/lang/erb.rb +30 -0
- data/lib/ruvim/lang/go.rb +83 -0
- data/lib/ruvim/lang/html.rb +34 -0
- data/lib/ruvim/lang/javascript.rb +83 -0
- data/lib/ruvim/lang/json.rb +6 -0
- data/lib/ruvim/lang/lua.rb +76 -0
- data/lib/ruvim/lang/makefile.rb +36 -0
- data/lib/ruvim/lang/markdown.rb +3 -4
- data/lib/ruvim/lang/ocaml.rb +77 -0
- data/lib/ruvim/lang/perl.rb +91 -0
- data/lib/ruvim/lang/python.rb +85 -0
- data/lib/ruvim/lang/registry.rb +102 -0
- data/lib/ruvim/lang/ruby.rb +7 -0
- data/lib/ruvim/lang/rust.rb +95 -0
- data/lib/ruvim/lang/scheme.rb +5 -0
- data/lib/ruvim/lang/sh.rb +76 -0
- data/lib/ruvim/lang/sql.rb +52 -0
- data/lib/ruvim/lang/toml.rb +36 -0
- data/lib/ruvim/lang/tsv.rb +4 -1
- data/lib/ruvim/lang/typescript.rb +53 -0
- data/lib/ruvim/lang/yaml.rb +62 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +14 -7
- data/lib/ruvim/screen.rb +126 -72
- data/lib/ruvim/stream/file_load.rb +85 -0
- data/lib/ruvim/stream/follow.rb +40 -0
- data/lib/ruvim/stream/git.rb +43 -0
- data/lib/ruvim/stream/run.rb +74 -0
- data/lib/ruvim/stream/stdin.rb +55 -0
- data/lib/ruvim/stream.rb +35 -0
- data/lib/ruvim/stream_mixer.rb +394 -0
- data/lib/ruvim/terminal.rb +18 -4
- data/lib/ruvim/text_metrics.rb +84 -65
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +5 -5
- data/lib/ruvim.rb +23 -6
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +43 -19
- data/test/app_dot_repeat_test.rb +27 -3
- data/test/app_ex_command_test.rb +154 -0
- data/test/app_motion_test.rb +13 -12
- data/test/app_register_test.rb +2 -1
- data/test/app_scenario_test.rb +15 -10
- data/test/app_startup_test.rb +70 -27
- data/test/app_text_object_test.rb +2 -1
- data/test/app_unicode_behavior_test.rb +3 -2
- data/test/browser_test.rb +88 -0
- data/test/buffer_test.rb +24 -0
- data/test/cli_test.rb +63 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/config_dsl_test.rb +47 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/ex_command_registry_test.rb +106 -0
- data/test/follow_test.rb +20 -21
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +96 -17
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +125 -0
- data/test/indent_test.rb +137 -0
- data/test/input_screen_integration_test.rb +1 -1
- data/test/keyword_chars_test.rb +85 -0
- data/test/lang_test.rb +634 -0
- data/test/markdown_renderer_test.rb +5 -5
- data/test/on_save_hook_test.rb +12 -8
- data/test/render_snapshot_test.rb +78 -0
- data/test/rich_view_test.rb +42 -42
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/stream_test.rb +165 -0
- data/test/window_test.rb +59 -0
- metadata +52 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab74dcac81f3fb559503edb3536f762424d68aa072d623dfd8a8ba4762bf4b81
|
|
4
|
+
data.tar.gz: 4444a1c49d93e791326c70ca0c93d1b092ccaec74fa689ebdf3b5981af2ec751
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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/
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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."
|