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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module RuVim
|
|
7
|
+
module Lang
|
|
8
|
+
module Ruby
|
|
9
|
+
PRISM_KEYWORD_TYPES = %i[
|
|
10
|
+
KEYWORD_ALIAS
|
|
11
|
+
KEYWORD_AND
|
|
12
|
+
KEYWORD_BEGIN
|
|
13
|
+
KEYWORD_BEGIN_UPCASE
|
|
14
|
+
KEYWORD_BREAK
|
|
15
|
+
KEYWORD_CASE
|
|
16
|
+
KEYWORD_CLASS
|
|
17
|
+
KEYWORD_DEF
|
|
18
|
+
KEYWORD_DEFINED
|
|
19
|
+
KEYWORD_DO
|
|
20
|
+
KEYWORD_ELSE
|
|
21
|
+
KEYWORD_ELSIF
|
|
22
|
+
KEYWORD_END
|
|
23
|
+
KEYWORD_ENSURE
|
|
24
|
+
KEYWORD_FALSE
|
|
25
|
+
KEYWORD_FOR
|
|
26
|
+
KEYWORD_IF
|
|
27
|
+
KEYWORD_IF_MODIFIER
|
|
28
|
+
KEYWORD_IN
|
|
29
|
+
KEYWORD_MODULE
|
|
30
|
+
KEYWORD_NEXT
|
|
31
|
+
KEYWORD_NIL
|
|
32
|
+
KEYWORD_NOT
|
|
33
|
+
KEYWORD_OR
|
|
34
|
+
KEYWORD_REDO
|
|
35
|
+
KEYWORD_RESCUE
|
|
36
|
+
KEYWORD_RESCUE_MODIFIER
|
|
37
|
+
KEYWORD_RETRY
|
|
38
|
+
KEYWORD_RETURN
|
|
39
|
+
KEYWORD_SELF
|
|
40
|
+
KEYWORD_SUPER
|
|
41
|
+
KEYWORD_THEN
|
|
42
|
+
KEYWORD_TRUE
|
|
43
|
+
KEYWORD_UNDEF
|
|
44
|
+
KEYWORD_UNLESS
|
|
45
|
+
KEYWORD_UNLESS_MODIFIER
|
|
46
|
+
KEYWORD_UNTIL
|
|
47
|
+
KEYWORD_UNTIL_MODIFIER
|
|
48
|
+
KEYWORD_WHEN
|
|
49
|
+
KEYWORD_WHILE
|
|
50
|
+
KEYWORD_WHILE_MODIFIER
|
|
51
|
+
KEYWORD_YIELD
|
|
52
|
+
MISSING
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
PRISM_STRING_TYPES = %i[
|
|
56
|
+
STRING_BEGIN
|
|
57
|
+
STRING_CONTENT
|
|
58
|
+
STRING_END
|
|
59
|
+
SYMBOL_BEGIN
|
|
60
|
+
REGEXP_BEGIN
|
|
61
|
+
REGEXP_CONTENT
|
|
62
|
+
REGEXP_END
|
|
63
|
+
XSTRING_BEGIN
|
|
64
|
+
XSTRING_CONTENT
|
|
65
|
+
XSTRING_END
|
|
66
|
+
WORDS_BEGIN
|
|
67
|
+
QWORDS_BEGIN
|
|
68
|
+
WORDS_SEPARATOR
|
|
69
|
+
STRING
|
|
70
|
+
CHARACTER_LITERAL
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
PRISM_NUMBER_TYPES = %i[
|
|
74
|
+
INTEGER
|
|
75
|
+
FLOAT
|
|
76
|
+
RATIONAL_NUMBER
|
|
77
|
+
IMAGINARY_NUMBER
|
|
78
|
+
UINTEGER
|
|
79
|
+
].freeze
|
|
80
|
+
|
|
81
|
+
PRISM_COMMENT_TYPES = %i[
|
|
82
|
+
COMMENT
|
|
83
|
+
EMBDOC_BEGIN
|
|
84
|
+
EMBDOC_LINE
|
|
85
|
+
EMBDOC_END
|
|
86
|
+
].freeze
|
|
87
|
+
|
|
88
|
+
PRISM_VARIABLE_TYPES = %i[
|
|
89
|
+
INSTANCE_VARIABLE
|
|
90
|
+
CLASS_VARIABLE
|
|
91
|
+
GLOBAL_VARIABLE
|
|
92
|
+
].freeze
|
|
93
|
+
|
|
94
|
+
PRISM_CONSTANT_TYPES = %i[
|
|
95
|
+
CONSTANT
|
|
96
|
+
].freeze
|
|
97
|
+
|
|
98
|
+
# Keywords that open a new indentation level
|
|
99
|
+
INDENT_OPEN_RE = /\A\s*(?:def|class|module|if|unless|while|until|for|begin|case)\b/
|
|
100
|
+
# `do` at end of line (block form)
|
|
101
|
+
INDENT_DO_RE = /\bdo\s*(\|[^|]*\|)?\s*$/
|
|
102
|
+
# Opening brackets at end of line
|
|
103
|
+
INDENT_BRACKET_OPEN_RE = /[\[({]\s*$/
|
|
104
|
+
# Keywords that close an indentation level
|
|
105
|
+
INDENT_CLOSE_RE = /\A\s*(?:end|[}\])])/
|
|
106
|
+
# Keywords at same level as their opening keyword (dedent for the line itself)
|
|
107
|
+
INDENT_MID_RE = /\A\s*(?:else|elsif|when|rescue|ensure|in)\b/
|
|
108
|
+
# Modifier keywords that do NOT open indentation
|
|
109
|
+
MODIFIER_RE = /\b(?:if|unless|while|until|rescue)\b/
|
|
110
|
+
|
|
111
|
+
module_function
|
|
112
|
+
|
|
113
|
+
def calculate_indent(lines, target_row, shiftwidth)
|
|
114
|
+
depth = 0
|
|
115
|
+
(0...target_row).each do |row|
|
|
116
|
+
line = lines[row]
|
|
117
|
+
stripped = line.to_s.lstrip
|
|
118
|
+
|
|
119
|
+
# Skip blank and comment lines for indent computation
|
|
120
|
+
next if stripped.empty? || stripped.start_with?("#")
|
|
121
|
+
|
|
122
|
+
# Check if the line opens a new level
|
|
123
|
+
if stripped.match?(INDENT_OPEN_RE)
|
|
124
|
+
# Check for modifier form: something before the keyword on the same line
|
|
125
|
+
# e.g. "return if true" — if keyword is not at start, it's a modifier
|
|
126
|
+
first_word = stripped[/\A(\w+)/, 1]
|
|
127
|
+
if %w[if unless while until rescue].include?(first_word)
|
|
128
|
+
depth += 1
|
|
129
|
+
elsif %w[def class module begin case for].include?(first_word)
|
|
130
|
+
depth += 1
|
|
131
|
+
end
|
|
132
|
+
elsif stripped.match?(INDENT_DO_RE)
|
|
133
|
+
depth += 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Count opening brackets (not at end-of-line pattern, but individual)
|
|
137
|
+
stripped.each_char do |ch|
|
|
138
|
+
case ch
|
|
139
|
+
when "{", "[", "("
|
|
140
|
+
depth += 1
|
|
141
|
+
when "}", "]", ")"
|
|
142
|
+
depth -= 1
|
|
143
|
+
end
|
|
144
|
+
end if !stripped.match?(INDENT_OPEN_RE) && !stripped.match?(INDENT_DO_RE)
|
|
145
|
+
|
|
146
|
+
# Handle closing keywords
|
|
147
|
+
if stripped.match?(/\A\s*end\b/)
|
|
148
|
+
depth -= 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Handle mid keywords (else/elsif/when/rescue/ensure) — they don't change depth for following lines
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Now compute indent for target_row
|
|
155
|
+
target_line = lines[target_row].to_s.lstrip
|
|
156
|
+
|
|
157
|
+
# If target line is a closing keyword, dedent
|
|
158
|
+
if target_line.match?(INDENT_CLOSE_RE)
|
|
159
|
+
depth -= 1
|
|
160
|
+
elsif target_line.match?(INDENT_MID_RE)
|
|
161
|
+
depth -= 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
depth = 0 if depth < 0
|
|
165
|
+
depth * shiftwidth
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns true if the line should increase indent for the next line
|
|
169
|
+
def indent_trigger?(line)
|
|
170
|
+
stripped = line.to_s.rstrip.lstrip
|
|
171
|
+
first_word = stripped[/\A(\w+)/, 1].to_s
|
|
172
|
+
return true if %w[def class module if unless while until for begin case].include?(first_word)
|
|
173
|
+
return true if stripped.match?(/\bdo\s*(\|[^|]*\|)?\s*$/)
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Dedent trigger patterns keyed by the last character typed
|
|
178
|
+
DEDENT_TRIGGERS = {
|
|
179
|
+
"d" => /\A(\s*)end\z/,
|
|
180
|
+
"e" => /\A(\s*)(?:else|rescue|ensure)\z/,
|
|
181
|
+
"f" => /\A(\s*)elsif\z/,
|
|
182
|
+
"n" => /\A(\s*)(?:when|in)\z/
|
|
183
|
+
}.freeze
|
|
184
|
+
|
|
185
|
+
# Returns the dedent pattern for the given character, or nil
|
|
186
|
+
def dedent_trigger(char)
|
|
187
|
+
DEDENT_TRIGGERS[char]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def on_save(ctx, path)
|
|
191
|
+
return unless path && File.exist?(path)
|
|
192
|
+
output, status = Open3.capture2e("ruby", "-wc", path)
|
|
193
|
+
message = output.sub(/^Syntax OK\n?\z/m, "").strip
|
|
194
|
+
|
|
195
|
+
if !status.success? || !message.empty?
|
|
196
|
+
buffer_id = ctx.buffer.id
|
|
197
|
+
items = message.lines.filter_map { |line|
|
|
198
|
+
if line =~ /\A.+?:(\d+):/
|
|
199
|
+
{ buffer_id: buffer_id, row: $1.to_i - 1, col: 0, text: line.strip }
|
|
200
|
+
end
|
|
201
|
+
}
|
|
202
|
+
items = [{ buffer_id: buffer_id, row: 0, col: 0, text: message }] if items.empty?
|
|
203
|
+
ctx.editor.set_quickfix_list(items)
|
|
204
|
+
first = message.lines.first.to_s.strip
|
|
205
|
+
hint = items.size > 1 ? " (Q to see all, #{items.size} total)" : ""
|
|
206
|
+
ctx.editor.echo_error("#{first}#{hint}")
|
|
207
|
+
else
|
|
208
|
+
ctx.editor.set_quickfix_list([])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def color_columns(text)
|
|
213
|
+
cols = {}
|
|
214
|
+
Prism.lex(text).value.each do |entry|
|
|
215
|
+
token = entry[0]
|
|
216
|
+
type = token.type
|
|
217
|
+
range = token.location.start_offset...token.location.end_offset
|
|
218
|
+
if PRISM_STRING_TYPES.include?(type)
|
|
219
|
+
range.each { |idx| cols[idx] = Highlighter::STRING_COLOR unless cols.key?(idx) }
|
|
220
|
+
elsif PRISM_KEYWORD_TYPES.include?(type)
|
|
221
|
+
range.each { |idx| cols[idx] = Highlighter::KEYWORD_COLOR unless cols.key?(idx) }
|
|
222
|
+
elsif PRISM_NUMBER_TYPES.include?(type)
|
|
223
|
+
range.each { |idx| cols[idx] = Highlighter::NUMBER_COLOR unless cols.key?(idx) }
|
|
224
|
+
elsif PRISM_VARIABLE_TYPES.include?(type)
|
|
225
|
+
range.each { |idx| cols[idx] = Highlighter::VARIABLE_COLOR unless cols.key?(idx) }
|
|
226
|
+
elsif PRISM_CONSTANT_TYPES.include?(type)
|
|
227
|
+
range.each { |idx| cols[idx] = Highlighter::CONSTANT_COLOR unless cols.key?(idx) }
|
|
228
|
+
elsif PRISM_COMMENT_TYPES.include?(type)
|
|
229
|
+
range.each { |idx| cols[idx] = Highlighter::COMMENT_COLOR }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
cols
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
module Lang
|
|
5
|
+
module Scheme
|
|
6
|
+
KEYWORDS = %w[
|
|
7
|
+
define define-syntax define-record-type define-values define-library
|
|
8
|
+
lambda let let* letrec letrec* let-values let-syntax letrec-syntax
|
|
9
|
+
if cond case when unless and or not
|
|
10
|
+
begin do set! quote quasiquote unquote unquote-splicing
|
|
11
|
+
syntax-rules syntax-case
|
|
12
|
+
import export library
|
|
13
|
+
else =>
|
|
14
|
+
call-with-current-continuation call/cc
|
|
15
|
+
call-with-values values
|
|
16
|
+
dynamic-wind
|
|
17
|
+
guard raise raise-continuable with-exception-handler
|
|
18
|
+
parameterize make-parameter
|
|
19
|
+
include include-ci
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
KEYWORD_RE = /(?<=[\s(])(#{KEYWORDS.map { |k| Regexp.escape(k) }.join("|")})(?=[\s()\]]|$)/
|
|
23
|
+
COMMENT_RE = /;.*/
|
|
24
|
+
STRING_RE = /"(?:\\.|[^"\\])*"/
|
|
25
|
+
CHAR_RE = /#\\(?:space|newline|tab|alarm|backspace|delete|escape|null|return|[^\s()])/
|
|
26
|
+
BOOLEAN_RE = /#[tf]\b/
|
|
27
|
+
NUMBER_RE = /(?<=[\s(])[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?(?=[\s()\]]|$)/
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def color_columns(text)
|
|
32
|
+
cols = {}
|
|
33
|
+
# Order matters: comment overrides all, then strings, then others
|
|
34
|
+
Highlighter.apply_regex(cols, text, CHAR_RE, Highlighter::STRING_COLOR)
|
|
35
|
+
Highlighter.apply_regex(cols, text, STRING_RE, Highlighter::STRING_COLOR)
|
|
36
|
+
Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
|
|
37
|
+
Highlighter.apply_regex(cols, text, BOOLEAN_RE, "\e[35m")
|
|
38
|
+
Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
|
|
39
|
+
Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
|
|
40
|
+
cols
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuVim
|
|
4
|
+
module Lang
|
|
5
|
+
module Tsv
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Detect TSV from buffer content: tabs >= commas and tabs > 0
|
|
9
|
+
def detect?(buffer)
|
|
10
|
+
sample = (0...[buffer.line_count, 20].min).map { |i| buffer.line_at(i) }
|
|
11
|
+
tabs = sample.sum { |l| l.count("\t") }
|
|
12
|
+
commas = sample.sum { |l| l.count(",") }
|
|
13
|
+
tabs > 0 && tabs >= commas
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
RichView.register("tsv", RichView::TableRenderer, detector: Lang::Tsv.method(:detect?))
|
|
19
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module RichView
|
|
7
|
+
module JsonRenderer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Signal that this renderer creates a virtual buffer
|
|
11
|
+
# instead of entering rich mode on the same buffer.
|
|
12
|
+
def open_view!(editor)
|
|
13
|
+
buffer = editor.current_buffer
|
|
14
|
+
window = editor.current_window
|
|
15
|
+
text = buffer.lines.join("\n")
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
parsed = JSON.parse(text)
|
|
19
|
+
rescue JSON::ParserError => e
|
|
20
|
+
editor.echo_error("JSON parse error: #{e.message}")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Compute cursor's significant char offset before formatting
|
|
25
|
+
cursor_offset = char_offset_for(buffer.lines, window.cursor_y, window.cursor_x)
|
|
26
|
+
sig_count = significant_char_count(text, cursor_offset + 1)
|
|
27
|
+
|
|
28
|
+
formatted = JSON.pretty_generate(parsed)
|
|
29
|
+
lines = formatted.lines(chomp: true)
|
|
30
|
+
|
|
31
|
+
target_line = line_for_significant_count(formatted, sig_count)
|
|
32
|
+
|
|
33
|
+
buf = editor.add_virtual_buffer(
|
|
34
|
+
kind: :json_formatted,
|
|
35
|
+
name: "[JSON Formatted]",
|
|
36
|
+
lines: lines,
|
|
37
|
+
filetype: "json",
|
|
38
|
+
readonly: true,
|
|
39
|
+
modifiable: false
|
|
40
|
+
)
|
|
41
|
+
editor.switch_to_buffer(buf.id)
|
|
42
|
+
RichView.bind_close_keys(editor, buf.id)
|
|
43
|
+
window.cursor_y = [target_line, lines.length - 1].min
|
|
44
|
+
window.cursor_x = 0
|
|
45
|
+
editor.echo("[JSON Formatted] #{lines.length} lines")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Count significant (non-whitespace-outside-strings) characters
|
|
49
|
+
# in text[0...byte_offset].
|
|
50
|
+
def significant_char_count(text, byte_offset)
|
|
51
|
+
in_string = false
|
|
52
|
+
escape = false
|
|
53
|
+
count = 0
|
|
54
|
+
text.each_char.with_index do |ch, i|
|
|
55
|
+
break if i >= byte_offset
|
|
56
|
+
if in_string
|
|
57
|
+
if escape
|
|
58
|
+
escape = false
|
|
59
|
+
elsif ch == "\\"
|
|
60
|
+
escape = true
|
|
61
|
+
elsif ch == '"'
|
|
62
|
+
in_string = false
|
|
63
|
+
end
|
|
64
|
+
count += 1
|
|
65
|
+
else
|
|
66
|
+
case ch
|
|
67
|
+
when '"'
|
|
68
|
+
in_string = true
|
|
69
|
+
count += 1
|
|
70
|
+
when " ", "\n", "\r", "\t"
|
|
71
|
+
# skip whitespace outside strings
|
|
72
|
+
else
|
|
73
|
+
count += 1
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Find the line number in text where the N-th significant character falls.
|
|
81
|
+
def line_for_significant_count(text, target_count)
|
|
82
|
+
return 0 if target_count <= 0
|
|
83
|
+
|
|
84
|
+
in_string = false
|
|
85
|
+
escape = false
|
|
86
|
+
count = 0
|
|
87
|
+
line = 0
|
|
88
|
+
text.each_char do |ch|
|
|
89
|
+
if ch == "\n" && !in_string
|
|
90
|
+
line += 1
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
if in_string
|
|
94
|
+
if escape
|
|
95
|
+
escape = false
|
|
96
|
+
elsif ch == "\\"
|
|
97
|
+
escape = true
|
|
98
|
+
elsif ch == '"'
|
|
99
|
+
in_string = false
|
|
100
|
+
end
|
|
101
|
+
count += 1
|
|
102
|
+
else
|
|
103
|
+
case ch
|
|
104
|
+
when '"'
|
|
105
|
+
in_string = true
|
|
106
|
+
count += 1
|
|
107
|
+
when " ", "\r", "\t"
|
|
108
|
+
# skip
|
|
109
|
+
else
|
|
110
|
+
count += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
return line if count >= target_count
|
|
114
|
+
end
|
|
115
|
+
line
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Compute character offset in joined text from cursor row/col.
|
|
119
|
+
def char_offset_for(lines, row, col)
|
|
120
|
+
offset = 0
|
|
121
|
+
lines.each_with_index do |line, i|
|
|
122
|
+
if i == row
|
|
123
|
+
return offset + [col, line.length].min
|
|
124
|
+
end
|
|
125
|
+
offset += line.length + 1 # +1 for "\n"
|
|
126
|
+
end
|
|
127
|
+
offset
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RuVim
|
|
6
|
+
module RichView
|
|
7
|
+
module JsonlRenderer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
SEPARATOR = "---"
|
|
11
|
+
|
|
12
|
+
def open_view!(editor)
|
|
13
|
+
buffer = editor.current_buffer
|
|
14
|
+
window = editor.current_window
|
|
15
|
+
cursor_row = window.cursor_y
|
|
16
|
+
|
|
17
|
+
output_lines = []
|
|
18
|
+
# Map from source line index to starting line in output
|
|
19
|
+
line_map = {}
|
|
20
|
+
|
|
21
|
+
buffer.lines.each_with_index do |raw_line, idx|
|
|
22
|
+
line = raw_line.to_s.strip
|
|
23
|
+
next if line.empty?
|
|
24
|
+
|
|
25
|
+
output_lines << SEPARATOR unless output_lines.empty?
|
|
26
|
+
line_map[idx] = output_lines.length
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
parsed = JSON.parse(line)
|
|
30
|
+
formatted = JSON.pretty_generate(parsed)
|
|
31
|
+
formatted.each_line(chomp: true) { |l| output_lines << l }
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
output_lines << "// PARSE ERROR: #{raw_line}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
output_lines << "" if output_lines.empty?
|
|
38
|
+
|
|
39
|
+
target_line = line_map[cursor_row] || 0
|
|
40
|
+
|
|
41
|
+
buf = editor.add_virtual_buffer(
|
|
42
|
+
kind: :jsonl_formatted,
|
|
43
|
+
name: "[JSONL Formatted]",
|
|
44
|
+
lines: output_lines,
|
|
45
|
+
filetype: "json",
|
|
46
|
+
readonly: true,
|
|
47
|
+
modifiable: false
|
|
48
|
+
)
|
|
49
|
+
editor.switch_to_buffer(buf.id)
|
|
50
|
+
RichView.bind_close_keys(editor, buf.id)
|
|
51
|
+
window.cursor_y = [target_line, output_lines.length - 1].min
|
|
52
|
+
window.cursor_x = 0
|
|
53
|
+
editor.echo("[JSONL Formatted] #{output_lines.length} lines")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|