ruvim 0.2.0 → 0.4.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/.github/workflows/test.yml +4 -0
- data/AGENTS.md +96 -0
- data/CLAUDE.md +1 -0
- data/README.md +15 -1
- data/docs/binding.md +39 -0
- data/docs/command.md +163 -4
- data/docs/config.md +12 -4
- data/docs/done.md +21 -0
- data/docs/spec.md +214 -18
- data/docs/todo.md +1 -5
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +105 -173
- data/lib/ruvim/app.rb +1165 -70
- data/lib/ruvim/buffer.rb +47 -1
- data/lib/ruvim/cli.rb +18 -3
- data/lib/ruvim/clipboard.rb +2 -0
- data/lib/ruvim/command_invocation.rb +3 -1
- data/lib/ruvim/command_line.rb +2 -0
- data/lib/ruvim/command_registry.rb +2 -0
- data/lib/ruvim/config_dsl.rb +2 -0
- data/lib/ruvim/config_loader.rb +2 -0
- data/lib/ruvim/context.rb +2 -0
- data/lib/ruvim/dispatcher.rb +143 -13
- data/lib/ruvim/display_width.rb +3 -0
- data/lib/ruvim/editor.rb +466 -71
- data/lib/ruvim/ex_command_registry.rb +2 -0
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +1066 -105
- data/lib/ruvim/highlighter.rb +19 -22
- data/lib/ruvim/input.rb +40 -28
- data/lib/ruvim/keymap_manager.rb +83 -0
- data/lib/ruvim/keyword_chars.rb +2 -0
- data/lib/ruvim/lang/base.rb +25 -0
- data/lib/ruvim/lang/csv.rb +18 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +52 -0
- data/lib/ruvim/lang/markdown.rb +170 -0
- data/lib/ruvim/lang/ruby.rb +236 -0
- data/lib/ruvim/lang/scheme.rb +44 -0
- data/lib/ruvim/lang/tsv.rb +19 -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/markdown_renderer.rb +248 -0
- data/lib/ruvim/rich_view/table_renderer.rb +176 -0
- data/lib/ruvim/rich_view.rb +109 -0
- data/lib/ruvim/screen.rb +503 -109
- data/lib/ruvim/terminal.rb +18 -1
- data/lib/ruvim/text_metrics.rb +2 -0
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim/window.rb +2 -0
- data/lib/ruvim.rb +24 -0
- data/test/app_completion_test.rb +98 -0
- data/test/app_dot_repeat_test.rb +13 -0
- data/test/app_motion_test.rb +13 -0
- data/test/app_scenario_test.rb +898 -1
- data/test/app_startup_test.rb +187 -0
- data/test/arglist_test.rb +113 -0
- data/test/buffer_test.rb +49 -30
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/dispatcher_test.rb +322 -0
- data/test/display_width_test.rb +41 -0
- data/test/editor_register_test.rb +23 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +165 -0
- data/test/indent_test.rb +287 -0
- data/test/input_screen_integration_test.rb +40 -2
- data/test/markdown_renderer_test.rb +279 -0
- data/test/on_save_hook_test.rb +150 -0
- data/test/rich_view_test.rb +734 -0
- data/test/screen_test.rb +304 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +49 -2
data/lib/ruvim/buffer.rb
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class Buffer
|
|
3
5
|
attr_reader :id, :kind, :name
|
|
4
6
|
attr_accessor :path
|
|
5
7
|
attr_reader :options
|
|
6
8
|
attr_writer :modified
|
|
9
|
+
attr_accessor :stream_state, :loading_state, :follow_backend
|
|
10
|
+
|
|
11
|
+
def stream_status
|
|
12
|
+
return nil unless @stream_state
|
|
13
|
+
|
|
14
|
+
if @kind == :stream
|
|
15
|
+
"stdin/#{@stream_state}"
|
|
16
|
+
elsif @follow_backend == :inotify
|
|
17
|
+
"follow/i"
|
|
18
|
+
else
|
|
19
|
+
"follow"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
attr_accessor :lang_module
|
|
7
23
|
|
|
8
24
|
def self.from_file(id:, path:)
|
|
9
25
|
lines =
|
|
@@ -54,12 +70,15 @@ module RuVim
|
|
|
54
70
|
@modified = false
|
|
55
71
|
@readonly = !!readonly
|
|
56
72
|
@modifiable = !!modifiable
|
|
73
|
+
@stream_state = nil
|
|
74
|
+
@loading_state = nil
|
|
57
75
|
@undo_stack = []
|
|
58
76
|
@redo_stack = []
|
|
59
77
|
@change_group_depth = 0
|
|
60
78
|
@group_before_snapshot = nil
|
|
61
79
|
@group_changed = false
|
|
62
80
|
@recording_suspended = false
|
|
81
|
+
@lang_module = Lang::Base
|
|
63
82
|
@options = {}
|
|
64
83
|
end
|
|
65
84
|
|
|
@@ -80,7 +99,7 @@ module RuVim
|
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def modifiable?
|
|
83
|
-
@modifiable
|
|
102
|
+
@modifiable && @loading_state != :live && @stream_state != :live
|
|
84
103
|
end
|
|
85
104
|
|
|
86
105
|
def modifiable=(value)
|
|
@@ -108,6 +127,8 @@ module RuVim
|
|
|
108
127
|
@name = name
|
|
109
128
|
@readonly = !!readonly
|
|
110
129
|
@modifiable = !!modifiable
|
|
130
|
+
@stream_state = nil unless @kind == :stream
|
|
131
|
+
@loading_state = nil
|
|
111
132
|
self
|
|
112
133
|
end
|
|
113
134
|
|
|
@@ -117,6 +138,8 @@ module RuVim
|
|
|
117
138
|
@path = nil
|
|
118
139
|
@readonly = false
|
|
119
140
|
@modifiable = true
|
|
141
|
+
@stream_state = nil
|
|
142
|
+
@loading_state = nil
|
|
120
143
|
@lines = [""]
|
|
121
144
|
@modified = false
|
|
122
145
|
@undo_stack.clear
|
|
@@ -329,6 +352,29 @@ module RuVim
|
|
|
329
352
|
@modified = true
|
|
330
353
|
end
|
|
331
354
|
|
|
355
|
+
# Append externally-streamed text without touching undo history or modifiable state.
|
|
356
|
+
def append_stream_text!(text)
|
|
357
|
+
chunk = text.to_s
|
|
358
|
+
return [@lines.length - 1, @lines[-1].length] if chunk.empty?
|
|
359
|
+
|
|
360
|
+
parts = chunk.split("\n", -1)
|
|
361
|
+
head = parts.shift || ""
|
|
362
|
+
@lines[-1] = @lines[-1].to_s + head
|
|
363
|
+
@lines.concat(parts)
|
|
364
|
+
@lines = [""] if @lines.empty?
|
|
365
|
+
@modified = false
|
|
366
|
+
[@lines.length - 1, @lines[-1].length]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def finalize_async_file_load!(ended_with_newline:)
|
|
370
|
+
if ended_with_newline && @lines.length > 1 && @lines[-1] == ""
|
|
371
|
+
@lines.pop
|
|
372
|
+
end
|
|
373
|
+
@lines = [""] if @lines.empty?
|
|
374
|
+
@modified = false
|
|
375
|
+
self
|
|
376
|
+
end
|
|
377
|
+
|
|
332
378
|
def write_to(path = nil)
|
|
333
379
|
raise RuVim::CommandError, "Buffer is readonly" if readonly?
|
|
334
380
|
|
data/lib/ruvim/cli.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class CLI
|
|
3
5
|
Options = Struct.new(
|
|
@@ -14,6 +16,7 @@ module RuVim
|
|
|
14
16
|
:no_swap,
|
|
15
17
|
:nomodifiable,
|
|
16
18
|
:restricted_mode,
|
|
19
|
+
:follow,
|
|
17
20
|
:verbose_level,
|
|
18
21
|
:startup_time_path,
|
|
19
22
|
:startup_open_layout,
|
|
@@ -38,18 +41,25 @@ module RuVim
|
|
|
38
41
|
return 0
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
if opts.files.length > 1 && opts.startup_open_layout.nil?
|
|
42
|
-
raise ParseError, "multiple files are not supported yet"
|
|
43
|
-
end
|
|
44
44
|
|
|
45
45
|
if opts.config_path && !File.file?(opts.config_path)
|
|
46
46
|
raise ParseError, "config file not found: #{opts.config_path}"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
ui_stdin = stdin
|
|
50
|
+
stdin_stream_mode = false
|
|
51
|
+
if stdin.respond_to?(:tty?) && !stdin.tty?
|
|
52
|
+
ui_stdin = IO.console
|
|
53
|
+
raise ParseError, "no controlling terminal available for interactive UI" unless ui_stdin
|
|
54
|
+
stdin_stream_mode = opts.files.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
49
57
|
app = RuVim::App.new(
|
|
50
58
|
path: opts.files.first,
|
|
51
59
|
paths: opts.files,
|
|
52
60
|
stdin: stdin,
|
|
61
|
+
ui_stdin: ui_stdin,
|
|
62
|
+
stdin_stream_mode: stdin_stream_mode,
|
|
53
63
|
stdout: stdout,
|
|
54
64
|
pre_config_actions: opts.pre_config_actions,
|
|
55
65
|
startup_actions: opts.startup_actions,
|
|
@@ -61,6 +71,7 @@ module RuVim
|
|
|
61
71
|
quickfix_errorfile: opts.quickfix_errorfile,
|
|
62
72
|
session_file: opts.session_file,
|
|
63
73
|
nomodifiable: opts.nomodifiable,
|
|
74
|
+
follow: opts.follow,
|
|
64
75
|
restricted: opts.restricted_mode,
|
|
65
76
|
verbose_level: opts.verbose_level,
|
|
66
77
|
verbose_io: stderr,
|
|
@@ -92,6 +103,7 @@ module RuVim
|
|
|
92
103
|
no_swap: false,
|
|
93
104
|
nomodifiable: false,
|
|
94
105
|
restricted_mode: false,
|
|
106
|
+
follow: false,
|
|
95
107
|
verbose_level: 0,
|
|
96
108
|
startup_time_path: nil,
|
|
97
109
|
startup_open_layout: nil,
|
|
@@ -150,6 +162,8 @@ module RuVim
|
|
|
150
162
|
end
|
|
151
163
|
when "-n"
|
|
152
164
|
opts.no_swap = true
|
|
165
|
+
when "-f"
|
|
166
|
+
opts.follow = true
|
|
153
167
|
when "-M"
|
|
154
168
|
opts.nomodifiable = true
|
|
155
169
|
when "-Z"
|
|
@@ -231,6 +245,7 @@ module RuVim
|
|
|
231
245
|
-h, --help Show this help
|
|
232
246
|
-v, --version Show version
|
|
233
247
|
--clean Start without user config and ftplugin
|
|
248
|
+
-f Open file in follow mode (tail -f style)
|
|
234
249
|
-R Open file readonly (disallow :w on current buffer)
|
|
235
250
|
-d Diff mode requested (compat placeholder; not implemented yet)
|
|
236
251
|
-q {errorfile} Quickfix startup placeholder (not implemented yet)
|
data/lib/ruvim/clipboard.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
class CommandInvocation
|
|
3
5
|
attr_accessor :id, :argv, :kwargs, :count, :bang, :raw_keys
|
|
@@ -6,7 +8,7 @@ module RuVim
|
|
|
6
8
|
@id = id
|
|
7
9
|
@argv = argv || []
|
|
8
10
|
@kwargs = kwargs || {}
|
|
9
|
-
@count = count
|
|
11
|
+
@count = count
|
|
10
12
|
@bang = !!bang
|
|
11
13
|
@raw_keys = raw_keys
|
|
12
14
|
end
|
data/lib/ruvim/command_line.rb
CHANGED
data/lib/ruvim/config_dsl.rb
CHANGED
data/lib/ruvim/config_loader.rb
CHANGED
data/lib/ruvim/context.rb
CHANGED
data/lib/ruvim/dispatcher.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "shellwords"
|
|
2
4
|
|
|
3
5
|
module RuVim
|
|
@@ -29,26 +31,40 @@ module RuVim
|
|
|
29
31
|
return
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
# Parse range prefix
|
|
35
|
+
range_result = parse_range(raw, editor)
|
|
36
|
+
rest = range_result ? range_result[:rest] : raw
|
|
37
|
+
|
|
38
|
+
# Try substitute on rest
|
|
39
|
+
if (sub = parse_substitute(rest))
|
|
40
|
+
kwargs = sub.merge(
|
|
41
|
+
range_start: range_result&.dig(:range_start),
|
|
42
|
+
range_end: range_result&.dig(:range_end)
|
|
43
|
+
)
|
|
44
|
+
invocation = CommandInvocation.new(id: "__substitute__", kwargs:)
|
|
34
45
|
ctx = Context.new(editor:, invocation:)
|
|
35
|
-
@command_host.ex_substitute(ctx, **
|
|
46
|
+
@command_host.ex_substitute(ctx, **kwargs)
|
|
36
47
|
editor.enter_normal_mode
|
|
37
48
|
return
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
parsed = parse_ex(
|
|
51
|
+
parsed = parse_ex(rest)
|
|
41
52
|
return if parsed.nil?
|
|
42
53
|
|
|
43
54
|
spec = @ex_registry.fetch(parsed.name)
|
|
44
55
|
validate_ex_args!(spec, parsed.argv, parsed.bang)
|
|
45
56
|
invocation = CommandInvocation.new(id: spec.name, argv: parsed.argv, bang: parsed.bang)
|
|
46
57
|
ctx = Context.new(editor:, invocation:)
|
|
47
|
-
|
|
58
|
+
range_kwargs = {}
|
|
59
|
+
if range_result
|
|
60
|
+
range_kwargs[:range_start] = range_result[:range_start]
|
|
61
|
+
range_kwargs[:range_end] = range_result[:range_end]
|
|
62
|
+
end
|
|
63
|
+
@command_host.call(spec.call, ctx, argv: parsed.argv, bang: parsed.bang, count: 1, kwargs: range_kwargs)
|
|
48
64
|
rescue StandardError => e
|
|
49
65
|
editor.echo_error("Error: #{e.message}")
|
|
50
66
|
ensure
|
|
51
|
-
editor.
|
|
67
|
+
editor.leave_command_line if editor.mode == :command_line
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def parse_ex(line)
|
|
@@ -66,28 +82,142 @@ module RuVim
|
|
|
66
82
|
raise RuVim::CommandError, "Parse error: #{e.message}"
|
|
67
83
|
end
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
# Parse a substitute command: s/pat/repl/flags
|
|
86
|
+
# Returns {pattern:, replacement:, flags_str:} or nil
|
|
87
|
+
def parse_substitute(line)
|
|
70
88
|
raw = line.to_s.strip
|
|
71
|
-
return nil unless raw.
|
|
72
|
-
return nil if raw.length <
|
|
89
|
+
return nil unless raw.match?(/\As[^a-zA-Z]/)
|
|
90
|
+
return nil if raw.length < 2
|
|
73
91
|
|
|
74
|
-
delim = raw[
|
|
92
|
+
delim = raw[1]
|
|
75
93
|
return nil if delim.nil? || delim =~ /\s/
|
|
76
|
-
i =
|
|
94
|
+
i = 2
|
|
77
95
|
pat, i = parse_delimited_segment(raw, i, delim)
|
|
78
96
|
return nil unless pat
|
|
79
97
|
rep, i = parse_delimited_segment(raw, i, delim)
|
|
80
98
|
return nil unless rep
|
|
81
|
-
|
|
99
|
+
flags_str = raw[i..].to_s
|
|
82
100
|
{
|
|
83
101
|
pattern: pat,
|
|
84
102
|
replacement: rep,
|
|
85
|
-
|
|
103
|
+
flags_str: flags_str
|
|
86
104
|
}
|
|
87
105
|
rescue StandardError
|
|
88
106
|
nil
|
|
89
107
|
end
|
|
90
108
|
|
|
109
|
+
# Parse an address at position pos in str.
|
|
110
|
+
# Returns [resolved_line_number, new_pos] or nil.
|
|
111
|
+
def parse_address(str, pos, editor)
|
|
112
|
+
return nil if pos >= str.length
|
|
113
|
+
|
|
114
|
+
ch = str[pos]
|
|
115
|
+
base = nil
|
|
116
|
+
new_pos = pos
|
|
117
|
+
|
|
118
|
+
case ch
|
|
119
|
+
when /\d/
|
|
120
|
+
# Numeric address
|
|
121
|
+
m = str[pos..].match(/\A(\d+)/)
|
|
122
|
+
return nil unless m
|
|
123
|
+
base = m[1].to_i - 1 # convert 1-based to 0-based
|
|
124
|
+
new_pos = pos + m[0].length
|
|
125
|
+
when "."
|
|
126
|
+
base = editor.current_window.cursor_y
|
|
127
|
+
new_pos = pos + 1
|
|
128
|
+
when "$"
|
|
129
|
+
base = editor.current_buffer.line_count - 1
|
|
130
|
+
new_pos = pos + 1
|
|
131
|
+
when "'"
|
|
132
|
+
# Mark address
|
|
133
|
+
mark_ch = str[pos + 1]
|
|
134
|
+
return nil unless mark_ch
|
|
135
|
+
if mark_ch == "<" || mark_ch == ">"
|
|
136
|
+
sel = editor.visual_selection
|
|
137
|
+
if sel
|
|
138
|
+
base = mark_ch == "<" ? sel[:start_row] : sel[:end_row]
|
|
139
|
+
else
|
|
140
|
+
return nil
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
loc = editor.mark_location(mark_ch)
|
|
144
|
+
return nil unless loc
|
|
145
|
+
base = loc[:row]
|
|
146
|
+
end
|
|
147
|
+
new_pos = pos + 2
|
|
148
|
+
when "+", "-"
|
|
149
|
+
# Relative offset with implicit current line
|
|
150
|
+
base = editor.current_window.cursor_y
|
|
151
|
+
# Don't advance new_pos — the offset parsing below will handle +/-
|
|
152
|
+
else
|
|
153
|
+
return nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Parse trailing +N / -N offsets
|
|
157
|
+
while new_pos < str.length
|
|
158
|
+
offset_ch = str[new_pos]
|
|
159
|
+
if offset_ch == "+"
|
|
160
|
+
m = str[new_pos + 1..].to_s.match(/\A(\d+)/)
|
|
161
|
+
if m
|
|
162
|
+
base += m[1].to_i
|
|
163
|
+
new_pos += 1 + m[0].length
|
|
164
|
+
else
|
|
165
|
+
base += 1
|
|
166
|
+
new_pos += 1
|
|
167
|
+
end
|
|
168
|
+
elsif offset_ch == "-"
|
|
169
|
+
m = str[new_pos + 1..].to_s.match(/\A(\d+)/)
|
|
170
|
+
if m
|
|
171
|
+
base -= m[1].to_i
|
|
172
|
+
new_pos += 1 + m[0].length
|
|
173
|
+
else
|
|
174
|
+
base -= 1
|
|
175
|
+
new_pos += 1
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
break
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Clamp to valid range
|
|
183
|
+
max_line = editor.current_buffer.line_count - 1
|
|
184
|
+
base = [[base, 0].max, max_line].min
|
|
185
|
+
|
|
186
|
+
[base, new_pos]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Parse a range from the beginning of raw.
|
|
190
|
+
# Returns {range_start:, range_end:, rest:} or nil.
|
|
191
|
+
def parse_range(raw, editor)
|
|
192
|
+
str = raw.to_s
|
|
193
|
+
return nil if str.empty?
|
|
194
|
+
|
|
195
|
+
# % = whole file
|
|
196
|
+
if str[0] == "%"
|
|
197
|
+
max_line = editor.current_buffer.line_count - 1
|
|
198
|
+
rest = str[1..].to_s
|
|
199
|
+
return { range_start: 0, range_end: max_line, rest: rest }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Try first address
|
|
203
|
+
addr1 = parse_address(str, 0, editor)
|
|
204
|
+
return nil unless addr1
|
|
205
|
+
|
|
206
|
+
line1, pos = addr1
|
|
207
|
+
|
|
208
|
+
if pos < str.length && str[pos] == ","
|
|
209
|
+
# addr,addr range
|
|
210
|
+
addr2 = parse_address(str, pos + 1, editor)
|
|
211
|
+
if addr2
|
|
212
|
+
line2, pos2 = addr2
|
|
213
|
+
return { range_start: line1, range_end: line2, rest: str[pos2..].to_s }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Single address
|
|
218
|
+
{ range_start: line1, range_end: line1, rest: str[pos..].to_s }
|
|
219
|
+
end
|
|
220
|
+
|
|
91
221
|
private
|
|
92
222
|
|
|
93
223
|
def parse_delimited_segment(str, idx, delim)
|
data/lib/ruvim/display_width.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RuVim
|
|
2
4
|
module DisplayWidth
|
|
3
5
|
module_function
|
|
@@ -11,6 +13,7 @@ module RuVim
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
code = ch.ord
|
|
16
|
+
return 1 if code <= 0xA0 && !code.zero?
|
|
14
17
|
return cached_codepoint_width(code) if codepoint_cacheable?(code)
|
|
15
18
|
|
|
16
19
|
uncached_codepoint_width(code)
|