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
data/lib/ruvim/stream.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class Stream
|
|
5
|
+
attr_accessor :state
|
|
6
|
+
attr_reader :stop_handler
|
|
7
|
+
|
|
8
|
+
def initialize(stop_handler: nil)
|
|
9
|
+
@state = nil
|
|
10
|
+
@stop_handler = stop_handler
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def live?
|
|
14
|
+
@state == :live
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def status
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def command
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stop!
|
|
26
|
+
# subclasses override
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require_relative "stream/stdin"
|
|
32
|
+
require_relative "stream/run"
|
|
33
|
+
require_relative "stream/follow"
|
|
34
|
+
require_relative "stream/file_load"
|
|
35
|
+
require_relative "stream/git"
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
class StreamMixer
|
|
5
|
+
LARGE_FILE_ASYNC_THRESHOLD_BYTES = 64 * 1024 * 1024
|
|
6
|
+
LARGE_FILE_STAGED_PREFIX_BYTES = 8 * 1024 * 1024
|
|
7
|
+
|
|
8
|
+
def initialize(editor:, signal_w:)
|
|
9
|
+
@editor = editor
|
|
10
|
+
@signal_w = signal_w
|
|
11
|
+
@stream_event_queue = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def prepare_stdin_stream_buffer!(io)
|
|
15
|
+
buf = @editor.current_buffer
|
|
16
|
+
if buf.intro_buffer?
|
|
17
|
+
@editor.materialize_intro_buffer!
|
|
18
|
+
buf = @editor.current_buffer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
buf.replace_all_lines!([""])
|
|
22
|
+
buf.configure_special!(kind: :stream, name: "[stdin]", readonly: true, modifiable: false)
|
|
23
|
+
buf.modified = false
|
|
24
|
+
buf.options["filetype"] = "text"
|
|
25
|
+
ensure_event_queue!
|
|
26
|
+
move_window_to_stream_end!(@editor.current_window, buf)
|
|
27
|
+
@editor.echo("[stdin] follow")
|
|
28
|
+
@pending_stdin = { buf: buf, io: io }
|
|
29
|
+
buf
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start_pending_stdin!
|
|
33
|
+
return unless @pending_stdin
|
|
34
|
+
|
|
35
|
+
ps = @pending_stdin
|
|
36
|
+
@pending_stdin = nil
|
|
37
|
+
buf = ps[:buf]
|
|
38
|
+
ensure_event_queue!
|
|
39
|
+
buf.stream = Stream::Stdin.new(
|
|
40
|
+
io: ps[:io], buffer_id: buf.id, queue: @stream_event_queue,
|
|
41
|
+
stop_handler: -> { stop_buffer_stream!(buf) }, &method(:notify_signal_wakeup)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start_command_stream!(buf, command)
|
|
46
|
+
ensure_event_queue!
|
|
47
|
+
buf.stream = Stream::Run.new(
|
|
48
|
+
command: command, buffer_id: buf.id, queue: @stream_event_queue,
|
|
49
|
+
stop_handler: -> { stop_buffer_stream!(buf) }, &method(:notify_signal_wakeup)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start_git_stream_command(buffer_id, cmd, root)
|
|
54
|
+
ensure_event_queue!
|
|
55
|
+
buf = @editor.buffers[buffer_id]
|
|
56
|
+
return unless buf
|
|
57
|
+
|
|
58
|
+
buf.stream = Stream::Git.new(cmd: cmd, root: root, buffer_id: buffer_id, queue: @stream_event_queue, &method(:notify_signal_wakeup))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stop_buffer_stream!(buf)
|
|
62
|
+
return false unless buf&.stream&.live?
|
|
63
|
+
|
|
64
|
+
buf.stream.stop!
|
|
65
|
+
@editor.echo("#{buf.display_name} stopped")
|
|
66
|
+
notify_signal_wakeup
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stop_git_stream!(buffer_id)
|
|
71
|
+
buf = @editor.buffers[buffer_id]
|
|
72
|
+
buf&.stream&.stop!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def drain_events!
|
|
76
|
+
return false unless @stream_event_queue
|
|
77
|
+
|
|
78
|
+
changed = false
|
|
79
|
+
loop do
|
|
80
|
+
event = @stream_event_queue.pop(true)
|
|
81
|
+
case event[:type]
|
|
82
|
+
when :stream_data
|
|
83
|
+
changed = apply_stream_chunk!(event[:buffer_id], event[:data]) || changed
|
|
84
|
+
when :stream_eof
|
|
85
|
+
changed = finish_stream!(event[:buffer_id], status: event[:status]) || changed
|
|
86
|
+
when :stream_error
|
|
87
|
+
changed = fail_stream!(event[:buffer_id], event[:error]) || changed
|
|
88
|
+
when :follow_data
|
|
89
|
+
changed = apply_stream_chunk!(event[:buffer_id], event[:data]) || changed
|
|
90
|
+
when :follow_truncated
|
|
91
|
+
if (buf = @editor.buffers[event[:buffer_id]])
|
|
92
|
+
@editor.echo("[follow] file truncated: #{buf.display_name}")
|
|
93
|
+
changed = true
|
|
94
|
+
end
|
|
95
|
+
when :follow_deleted
|
|
96
|
+
if (buf = @editor.buffers[event[:buffer_id]])
|
|
97
|
+
@editor.echo("[follow] file deleted, waiting for re-creation: #{buf.display_name}")
|
|
98
|
+
changed = true
|
|
99
|
+
end
|
|
100
|
+
when :file_lines
|
|
101
|
+
changed = apply_async_file_lines!(event[:buffer_id], event[:head], event[:lines], loaded_bytes: event[:loaded_bytes], file_size: event[:file_size]) || changed
|
|
102
|
+
when :file_eof
|
|
103
|
+
changed = finish_async_file_load!(event[:buffer_id], ended_with_newline: event[:ended_with_newline]) || changed
|
|
104
|
+
when :file_error
|
|
105
|
+
changed = fail_async_file_load!(event[:buffer_id], event[:error]) || changed
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue ThreadError
|
|
109
|
+
changed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def ex_follow_toggle
|
|
113
|
+
buf = @editor.current_buffer
|
|
114
|
+
raise RuVim::CommandError, "No file associated with buffer" unless buf.path
|
|
115
|
+
|
|
116
|
+
if buf.stream.is_a?(Stream::Follow)
|
|
117
|
+
stop_follow!(buf)
|
|
118
|
+
else
|
|
119
|
+
raise RuVim::CommandError, "Buffer has unsaved changes" if buf.modified?
|
|
120
|
+
start_follow!(buf)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def start_follow!(buf)
|
|
125
|
+
ensure_event_queue!
|
|
126
|
+
Buffer.ensure_regular_file!(buf.path) if buf.path
|
|
127
|
+
|
|
128
|
+
if buf.path && File.file?(buf.path)
|
|
129
|
+
data = File.binread(buf.path)
|
|
130
|
+
if data.end_with?("\n") && buf.lines.last.to_s != ""
|
|
131
|
+
following_wins = @editor.windows.values.select do |w|
|
|
132
|
+
w.buffer_id == buf.id && stream_window_following_end?(w, buf)
|
|
133
|
+
end
|
|
134
|
+
buf.append_stream_text!("\n")
|
|
135
|
+
following_wins.each { |w| move_window_to_stream_end!(w, buf) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
buf.stream = Stream::Follow.new(
|
|
140
|
+
path: buf.path, buffer_id: buf.id, queue: @stream_event_queue,
|
|
141
|
+
stop_handler: -> { stop_follow!(buf) }, &method(:notify_signal_wakeup)
|
|
142
|
+
)
|
|
143
|
+
@editor.echo("[follow] #{buf.display_name}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def stop_follow!(buf)
|
|
147
|
+
buf.stream&.stop!
|
|
148
|
+
# Remove trailing empty line added as sentinel by start_follow!
|
|
149
|
+
if buf.line_count > 1 && buf.lines.last.to_s == ""
|
|
150
|
+
buf.lines.pop
|
|
151
|
+
last = buf.line_count - 1
|
|
152
|
+
@editor.windows.each_value do |win|
|
|
153
|
+
next unless win.buffer_id == buf.id
|
|
154
|
+
win.cursor_y = last if win.cursor_y > last
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
buf.stream = nil
|
|
158
|
+
@editor.echo("[follow] stopped")
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def follow_active?(buf)
|
|
163
|
+
buf.stream.is_a?(Stream::Follow)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def open_path_with_large_file_support(path)
|
|
167
|
+
return @editor.open_path_sync(path) unless should_open_path_async?(path)
|
|
168
|
+
return @editor.open_path_sync(path) unless can_start_async_file_load?
|
|
169
|
+
|
|
170
|
+
open_path_asynchronously!(path)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def shutdown!
|
|
174
|
+
@editor.buffers.each_value do |buf|
|
|
175
|
+
buf.stream&.stop!
|
|
176
|
+
rescue StandardError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def ensure_event_queue!
|
|
182
|
+
@stream_event_queue ||= Queue.new
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def apply_stream_chunk!(buffer_id, text)
|
|
188
|
+
return false if text.to_s.empty?
|
|
189
|
+
|
|
190
|
+
buf = @editor.buffers[buffer_id]
|
|
191
|
+
return false unless buf
|
|
192
|
+
|
|
193
|
+
following_win_ids = @editor.windows.values.filter_map do |win|
|
|
194
|
+
next unless win.buffer_id == buf.id
|
|
195
|
+
next unless stream_window_following_end?(win, buf)
|
|
196
|
+
win.id
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
buf.append_stream_text!(text)
|
|
200
|
+
|
|
201
|
+
following_win_ids.each do |win_id|
|
|
202
|
+
win = @editor.windows[win_id]
|
|
203
|
+
move_window_to_stream_end!(win, buf) if win
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def finish_stream!(buffer_id, status: nil)
|
|
210
|
+
buf = @editor.buffers[buffer_id]
|
|
211
|
+
return false unless buf&.stream
|
|
212
|
+
|
|
213
|
+
stream = buf.stream
|
|
214
|
+
stream.thread = nil if stream.respond_to?(:thread=)
|
|
215
|
+
stream.io = nil if stream.respond_to?(:io=)
|
|
216
|
+
|
|
217
|
+
# Remove trailing empty line if present
|
|
218
|
+
if buf.lines.length > 1 && buf.lines[-1] == ""
|
|
219
|
+
buf.lines.pop
|
|
220
|
+
end
|
|
221
|
+
stream.state = :closed
|
|
222
|
+
stream.exit_status = status if stream.respond_to?(:exit_status=)
|
|
223
|
+
|
|
224
|
+
if status
|
|
225
|
+
@editor.echo("#{buf.display_name} exit #{status.exitstatus}")
|
|
226
|
+
elsif buf.kind == :stream
|
|
227
|
+
@editor.echo("[stdin] EOF")
|
|
228
|
+
else
|
|
229
|
+
@editor.echo("#{buf.display_name} #{buf.line_count} lines")
|
|
230
|
+
end
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def fail_stream!(buffer_id, error)
|
|
235
|
+
buf = @editor.buffers[buffer_id]
|
|
236
|
+
return false unless buf&.stream
|
|
237
|
+
|
|
238
|
+
# Ignore errors from intentionally closed streams
|
|
239
|
+
if buf.stream.state == :closed
|
|
240
|
+
msg = error.to_s.downcase
|
|
241
|
+
return false if msg.include?("stream closed") || msg.include?("closed in another thread")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
stream = buf.stream
|
|
245
|
+
stream.thread = nil if stream.respond_to?(:thread=)
|
|
246
|
+
stream.io = nil if stream.respond_to?(:io=)
|
|
247
|
+
stream.state = :error
|
|
248
|
+
@editor.echo_error("#{buf.display_name} stream error: #{error}")
|
|
249
|
+
true
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def apply_async_file_lines!(buffer_id, head, lines, loaded_bytes: nil, file_size: nil)
|
|
253
|
+
buf = @editor.buffers[buffer_id]
|
|
254
|
+
return false unless buf
|
|
255
|
+
|
|
256
|
+
following_win_ids = if buf.stream.is_a?(Stream::Follow)
|
|
257
|
+
@editor.windows.values.filter_map do |win|
|
|
258
|
+
next unless win.buffer_id == buffer_id
|
|
259
|
+
next unless stream_window_following_end?(win, buf)
|
|
260
|
+
win.id
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
buf.append_stream_lines!(head, lines)
|
|
265
|
+
|
|
266
|
+
following_win_ids&.each do |win_id|
|
|
267
|
+
win = @editor.windows[win_id]
|
|
268
|
+
move_window_to_stream_end!(win, buf) if win
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if loaded_bytes && file_size && file_size > 0
|
|
272
|
+
pct = (loaded_bytes * 100.0 / file_size).clamp(0, 100)
|
|
273
|
+
@editor.echo(format("\"%s\" loading... %d%%", buf.display_name, pct))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
true
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def finish_async_file_load!(buffer_id, ended_with_newline:)
|
|
280
|
+
buf = @editor.buffers[buffer_id]
|
|
281
|
+
return false unless buf
|
|
282
|
+
|
|
283
|
+
buf.finalize_async_file_load!(ended_with_newline: !!ended_with_newline)
|
|
284
|
+
buf.stream.state = :closed if buf.stream
|
|
285
|
+
@editor.echo(format("\"%s\" %dL", buf.display_name, buf.line_count))
|
|
286
|
+
true
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def fail_async_file_load!(buffer_id, error)
|
|
290
|
+
buf = @editor.buffers[buffer_id]
|
|
291
|
+
if buf&.stream
|
|
292
|
+
buf.stream.state = :error
|
|
293
|
+
end
|
|
294
|
+
@editor.echo_error("\"#{(buf && buf.display_name) || buffer_id}\" load error: #{error}")
|
|
295
|
+
true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def stream_window_following_end?(win, buf)
|
|
299
|
+
return false unless win
|
|
300
|
+
|
|
301
|
+
last_row = buf.line_count - 1
|
|
302
|
+
win.cursor_y >= last_row
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def move_window_to_stream_end!(win, buf)
|
|
306
|
+
return unless win && buf
|
|
307
|
+
|
|
308
|
+
last_row = buf.line_count - 1
|
|
309
|
+
win.cursor_y = last_row
|
|
310
|
+
win.cursor_x = buf.line_length(last_row)
|
|
311
|
+
win.clamp_to_buffer(buf)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def should_open_path_async?(path)
|
|
315
|
+
p = path.to_s
|
|
316
|
+
return false if p.empty?
|
|
317
|
+
return false unless File.file?(p)
|
|
318
|
+
|
|
319
|
+
File.size(p) >= large_file_async_threshold_bytes
|
|
320
|
+
rescue StandardError
|
|
321
|
+
false
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def can_start_async_file_load?
|
|
325
|
+
@editor.buffers.none? { |_, buf| buf.stream.is_a?(Stream::FileLoad) && buf.stream.live? }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def large_file_async_threshold_bytes
|
|
329
|
+
raw = ENV["RUVIM_ASYNC_FILE_THRESHOLD_BYTES"]
|
|
330
|
+
n = raw.to_i if raw
|
|
331
|
+
return n if n && n.positive?
|
|
332
|
+
|
|
333
|
+
LARGE_FILE_ASYNC_THRESHOLD_BYTES
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def open_path_asynchronously!(path)
|
|
337
|
+
file_size = File.size(path)
|
|
338
|
+
buf = @editor.add_empty_buffer(path: path)
|
|
339
|
+
@editor.switch_to_buffer(buf.id)
|
|
340
|
+
buf.modified = false
|
|
341
|
+
|
|
342
|
+
ensure_event_queue!
|
|
343
|
+
io = File.open(path, "rb")
|
|
344
|
+
staged_prefix_bytes = async_file_staged_prefix_bytes
|
|
345
|
+
staged_mode = file_size > staged_prefix_bytes
|
|
346
|
+
if staged_mode
|
|
347
|
+
prefix = io.read(staged_prefix_bytes) || "".b
|
|
348
|
+
unless prefix.empty?
|
|
349
|
+
last_nl = prefix.rindex("\n".b)
|
|
350
|
+
if last_nl && last_nl < prefix.bytesize - 1
|
|
351
|
+
remainder = prefix.bytesize - last_nl - 1
|
|
352
|
+
prefix = prefix[0..last_nl]
|
|
353
|
+
io.seek(-remainder, IO::SEEK_CUR)
|
|
354
|
+
end
|
|
355
|
+
buf.append_stream_text!(Buffer.decode_text(prefix))
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
if io.eof?
|
|
360
|
+
buf.finalize_async_file_load!(ended_with_newline: prefix&.end_with?("\n") || false)
|
|
361
|
+
io.close unless io.closed?
|
|
362
|
+
return buf
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Create FileLoad stream after prefix reading; starts background thread immediately
|
|
366
|
+
buf.stream = Stream::FileLoad.new(io: io, file_size: file_size, buffer_id: buf.id, queue: @stream_event_queue, &method(:notify_signal_wakeup))
|
|
367
|
+
|
|
368
|
+
size_mb = file_size.fdiv(1024 * 1024)
|
|
369
|
+
if staged_mode
|
|
370
|
+
@editor.echo(format("\"%s\" loading... (showing first %.0fMB of %.1fMB)", path, staged_prefix_bytes.fdiv(1024 * 1024), size_mb))
|
|
371
|
+
else
|
|
372
|
+
@editor.echo(format("\"%s\" loading... (%.1fMB)", path, size_mb))
|
|
373
|
+
end
|
|
374
|
+
buf
|
|
375
|
+
rescue StandardError
|
|
376
|
+
buf.stream = nil if buf
|
|
377
|
+
raise
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def async_file_staged_prefix_bytes
|
|
381
|
+
raw = ENV["RUVIM_ASYNC_FILE_PREFIX_BYTES"]
|
|
382
|
+
n = raw.to_i if raw
|
|
383
|
+
return n if n && n.positive?
|
|
384
|
+
|
|
385
|
+
LARGE_FILE_STAGED_PREFIX_BYTES
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def notify_signal_wakeup
|
|
389
|
+
@signal_w.write_nonblock(".")
|
|
390
|
+
rescue IO::WaitWritable, Errno::EPIPE
|
|
391
|
+
nil
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
data/lib/ruvim/terminal.rb
CHANGED
|
@@ -22,17 +22,31 @@ module RuVim
|
|
|
22
22
|
|
|
23
23
|
def with_ui
|
|
24
24
|
@stdin.raw do
|
|
25
|
-
write("\e]112\a\e[?1049h\e[?25l")
|
|
25
|
+
write("\e]112\a\e[2 q\e[?1049h\e[?25l")
|
|
26
26
|
yield
|
|
27
27
|
ensure
|
|
28
|
-
write("\e[?25h\e[?1049l")
|
|
28
|
+
write("\e[0 q\e[?25h\e[?1049l")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def suspend_for_shell(command)
|
|
33
|
+
shell = ENV["SHELL"].to_s
|
|
34
|
+
shell = "/bin/sh" if shell.empty?
|
|
35
|
+
@stdin.cooked do
|
|
36
|
+
write("\e[0 q\e[?25h\e[?1049l")
|
|
37
|
+
system(shell, "-c", command)
|
|
38
|
+
status = $?
|
|
39
|
+
write("\r\nPress ENTER or type command to continue")
|
|
40
|
+
@stdin.raw { @stdin.getc }
|
|
41
|
+
write("\e[2 q\e[?1049h\e[?25l")
|
|
42
|
+
status
|
|
29
43
|
end
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
def suspend_for_tstp
|
|
33
47
|
prev_tstp = Signal.trap("TSTP", "DEFAULT")
|
|
34
48
|
@stdin.cooked do
|
|
35
|
-
write("\e[?25h\e[?1049l")
|
|
49
|
+
write("\e[0 q\e[?25h\e[?1049l")
|
|
36
50
|
Process.kill("TSTP", 0)
|
|
37
51
|
end
|
|
38
52
|
ensure
|
|
@@ -41,7 +55,7 @@ module RuVim
|
|
|
41
55
|
rescue StandardError
|
|
42
56
|
nil
|
|
43
57
|
end
|
|
44
|
-
write("\e[?1049h\e[?25l")
|
|
58
|
+
write("\e[2 q\e[?1049h\e[?25l")
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
end
|
data/lib/ruvim/text_metrics.rb
CHANGED
|
@@ -4,15 +4,21 @@ module RuVim
|
|
|
4
4
|
module TextMetrics
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
Cell
|
|
7
|
+
class Cell
|
|
8
|
+
attr_reader :glyph, :source_col, :display_width
|
|
9
|
+
|
|
10
|
+
def initialize(glyph, source_col, display_width)
|
|
11
|
+
@glyph = glyph
|
|
12
|
+
@source_col = source_col
|
|
13
|
+
@display_width = display_width
|
|
14
|
+
end
|
|
15
|
+
end
|
|
8
16
|
|
|
9
|
-
# Cursor positions in RuVim are currently "character index" (Ruby String#[] index on UTF-8),
|
|
10
|
-
# not byte offsets. Grapheme-aware movement is layered on top of that.
|
|
11
17
|
def previous_grapheme_char_index(line, char_index)
|
|
12
|
-
idx = [char_index
|
|
18
|
+
idx = [char_index, 0].max
|
|
13
19
|
return 0 if idx <= 0
|
|
14
20
|
|
|
15
|
-
left = line
|
|
21
|
+
left = line[0...idx].to_s
|
|
16
22
|
clusters = left.scan(/\X/)
|
|
17
23
|
return 0 if clusters.empty?
|
|
18
24
|
|
|
@@ -21,7 +27,7 @@ module RuVim
|
|
|
21
27
|
|
|
22
28
|
def next_grapheme_char_index(line, char_index)
|
|
23
29
|
s = line.to_s
|
|
24
|
-
idx = [[char_index
|
|
30
|
+
idx = [[char_index, 0].max, s.length].min
|
|
25
31
|
return s.length if idx >= s.length
|
|
26
32
|
|
|
27
33
|
rest = s[idx..].to_s
|
|
@@ -32,93 +38,106 @@ module RuVim
|
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def screen_col_for_char_index(line, char_index, tabstop: 2)
|
|
35
|
-
idx = [char_index
|
|
36
|
-
prefix = line
|
|
41
|
+
idx = [char_index, 0].max
|
|
42
|
+
prefix = line[0...idx].to_s
|
|
37
43
|
RuVim::DisplayWidth.display_width(prefix, tabstop:)
|
|
38
44
|
end
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
43
|
-
s = line.to_s
|
|
44
|
-
target = [target_screen_col.to_i, 0].max
|
|
45
|
-
screen_col = 0
|
|
46
|
-
char_index = 0
|
|
47
|
-
|
|
48
|
-
s.scan(/\X/).each do |cluster|
|
|
49
|
-
width = RuVim::DisplayWidth.display_width(cluster, tabstop:, start_col: screen_col)
|
|
50
|
-
if screen_col + width > target
|
|
51
|
-
return align == :ceil ? (char_index + cluster.length) : char_index
|
|
52
|
-
end
|
|
46
|
+
if defined?(RuVim::TextMetricsExt)
|
|
47
|
+
# ---- C extension paths ----
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
def clip_cells_for_width(text, width, source_col_start: 0, tabstop: 2)
|
|
50
|
+
TextMetricsExt.clip_cells_for_width(text, width, source_col_start:, tabstop:)
|
|
56
51
|
end
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
54
|
+
TextMetricsExt.char_index_for_screen_col(line, target_screen_col, tabstop:, align:)
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
# ---- Pure Ruby fallback ----
|
|
58
|
+
|
|
59
|
+
def char_index_for_screen_col(line, target_screen_col, tabstop: 2, align: :floor)
|
|
60
|
+
s = line.to_s
|
|
61
|
+
target = [target_screen_col, 0].max
|
|
62
|
+
screen_col = 0
|
|
63
|
+
char_index = 0
|
|
64
|
+
|
|
65
|
+
s.scan(/\X/).each do |cluster|
|
|
66
|
+
width = RuVim::DisplayWidth.display_width(cluster, tabstop:, start_col: screen_col)
|
|
67
|
+
if screen_col + width > target
|
|
68
|
+
return align == :ceil ? (char_index + cluster.length) : char_index
|
|
69
|
+
end
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
display_col = 0
|
|
65
|
-
source_col = source_col_start.to_i
|
|
71
|
+
screen_col += width
|
|
72
|
+
char_index += cluster.length
|
|
73
|
+
end
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
|
|
70
|
-
break if display_col + w > max_width
|
|
75
|
+
char_index
|
|
76
|
+
end
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
def clip_cells_for_width(text, width, source_col_start: 0, tabstop: 2)
|
|
79
|
+
max_width = [width, 0].max
|
|
80
|
+
cells = []
|
|
81
|
+
display_col = 0
|
|
82
|
+
source_col = source_col_start
|
|
83
|
+
|
|
84
|
+
text.to_s.each_char do |ch|
|
|
85
|
+
code = ch.ord
|
|
86
|
+
# Fast path: printable ASCII (0x20..0x7E) — width 1, no special handling
|
|
87
|
+
if code >= 0x20 && code <= 0x7E
|
|
88
|
+
break if display_col >= max_width
|
|
89
|
+
cells << Cell.new(ch, source_col, 1)
|
|
90
|
+
display_col += 1
|
|
91
|
+
source_col += 1
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if ch == "\t"
|
|
96
|
+
w = tabstop - (display_col % tabstop)
|
|
97
|
+
w = tabstop if w.zero?
|
|
98
|
+
break if display_col + w > max_width
|
|
99
|
+
|
|
100
|
+
w.times do
|
|
101
|
+
cells << Cell.new(" ", source_col, 1)
|
|
102
|
+
end
|
|
103
|
+
display_col += w
|
|
104
|
+
source_col += 1
|
|
105
|
+
next
|
|
74
106
|
end
|
|
75
|
-
display_col += w
|
|
76
|
-
source_col += 1
|
|
77
|
-
next
|
|
78
|
-
end
|
|
79
107
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
# Control chars (0x00..0x1F, 0x7F, 0x80..0x9F)
|
|
109
|
+
if code < 0x20 || code == 0x7F || (code >= 0x80 && code <= 0x9F)
|
|
110
|
+
break if display_col >= max_width
|
|
111
|
+
cells << Cell.new("?", source_col, 1)
|
|
112
|
+
display_col += 1
|
|
113
|
+
source_col += 1
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
w = RuVim::DisplayWidth.cell_width(ch, col: display_col, tabstop:)
|
|
83
118
|
break if display_col + w > max_width
|
|
84
119
|
|
|
85
|
-
cells << Cell.new(
|
|
120
|
+
cells << Cell.new(ch, source_col, w)
|
|
86
121
|
display_col += w
|
|
87
122
|
source_col += 1
|
|
88
|
-
next
|
|
89
123
|
end
|
|
90
|
-
break if display_col + w > max_width
|
|
91
124
|
|
|
92
|
-
cells
|
|
93
|
-
display_col += w
|
|
94
|
-
source_col += 1
|
|
125
|
+
[cells, display_col]
|
|
95
126
|
end
|
|
96
|
-
|
|
97
|
-
[cells, display_col]
|
|
98
127
|
end
|
|
99
128
|
|
|
100
129
|
def pad_plain_to_screen_width(text, width, tabstop: 2)
|
|
101
130
|
cells, used = clip_cells_for_width(text, width, tabstop:)
|
|
102
131
|
out = cells.map(&:glyph).join
|
|
103
|
-
out << (" " * [width
|
|
132
|
+
out << (" " * [width - used, 0].max)
|
|
104
133
|
out
|
|
105
134
|
end
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
text.to_s.each_char.map { |ch| terminal_unsafe_control_char?(ch) ? terminal_safe_placeholder(ch) : ch }.join
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def terminal_unsafe_control_char?(ch)
|
|
112
|
-
return false if ch.nil? || ch.empty? || ch == "\t"
|
|
136
|
+
UNSAFE_CONTROL_CHAR_RE = /[\u0000-\u0008\u000a-\u001f\u007f\u0080-\u009f]/
|
|
113
137
|
|
|
114
|
-
|
|
115
|
-
(
|
|
116
|
-
rescue StandardError
|
|
117
|
-
false
|
|
138
|
+
def terminal_safe_text(text)
|
|
139
|
+
text.to_s.gsub(UNSAFE_CONTROL_CHAR_RE, "?")
|
|
118
140
|
end
|
|
119
141
|
|
|
120
|
-
def terminal_safe_placeholder(_ch)
|
|
121
|
-
"?"
|
|
122
|
-
end
|
|
123
142
|
end
|
|
124
143
|
end
|
data/lib/ruvim/version.rb
CHANGED