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.
- checksums.yaml +4 -4
- data/AGENTS.md +68 -7
- data/README.md +30 -7
- 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 +18 -1
- data/docs/command.md +156 -10
- data/docs/config.md +10 -2
- data/docs/done.md +23 -0
- data/docs/spec.md +162 -25
- data/docs/todo.md +9 -0
- data/docs/tutorial.md +33 -1
- data/docs/vim_diff.md +31 -8
- data/ext/ruvim/extconf.rb +5 -0
- data/ext/ruvim/ruvim_ext.c +519 -0
- data/lib/ruvim/app.rb +246 -2525
- data/lib/ruvim/browser.rb +104 -0
- data/lib/ruvim/buffer.rb +43 -20
- data/lib/ruvim/cli.rb +6 -0
- 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 +74 -80
- data/lib/ruvim/ex_command_registry.rb +3 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/gh/link.rb +207 -0
- data/lib/ruvim/git/blame.rb +255 -0
- data/lib/ruvim/git/branch.rb +112 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/grep.rb +107 -0
- data/lib/ruvim/git/handler.rb +125 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +351 -77
- data/lib/ruvim/highlighter.rb +4 -11
- data/lib/ruvim/input.rb +1 -0
- 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 +43 -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 +40 -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/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view/table_renderer.rb +3 -3
- data/lib/ruvim/rich_view.rb +30 -7
- data/lib/ruvim/screen.rb +135 -84
- 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 +31 -4
- data/test/app_command_test.rb +382 -0
- data/test/app_completion_test.rb +65 -16
- 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 +182 -8
- 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 +77 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_invocation_test.rb +33 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +134 -0
- data/test/dispatcher_test.rb +74 -4
- data/test/display_width_test.rb +41 -0
- data/test/ex_command_registry_test.rb +106 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +198 -0
- data/test/gh_link_test.rb +141 -0
- data/test/git_blame_test.rb +792 -0
- data/test/git_grep_test.rb +64 -0
- data/test/highlighter_test.rb +169 -0
- data/test/indent_test.rb +223 -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 +279 -23
- data/test/run_command_test.rb +307 -0
- data/test/screen_test.rb +68 -5
- data/test/search_option_test.rb +19 -0
- data/test/stream_test.rb +165 -0
- data/test/test_helper.rb +9 -0
- data/test/window_test.rb +59 -0
- metadata +68 -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
|
@@ -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
|
|
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
|
-
-
|
|
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 -
|
|
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
|
-
-
|
|
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."
|