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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +68 -7
  3. data/README.md +30 -7
  4. data/Rakefile +7 -0
  5. data/benchmark/cext_compare.rb +165 -0
  6. data/benchmark/chunked_load.rb +256 -0
  7. data/benchmark/file_load.rb +140 -0
  8. data/benchmark/hotspots.rb +178 -0
  9. data/docs/binding.md +18 -1
  10. data/docs/command.md +156 -10
  11. data/docs/config.md +10 -2
  12. data/docs/done.md +23 -0
  13. data/docs/spec.md +162 -25
  14. data/docs/todo.md +9 -0
  15. data/docs/tutorial.md +33 -1
  16. data/docs/vim_diff.md +31 -8
  17. data/ext/ruvim/extconf.rb +5 -0
  18. data/ext/ruvim/ruvim_ext.c +519 -0
  19. data/lib/ruvim/app.rb +246 -2525
  20. data/lib/ruvim/browser.rb +104 -0
  21. data/lib/ruvim/buffer.rb +43 -20
  22. data/lib/ruvim/cli.rb +6 -0
  23. data/lib/ruvim/command_invocation.rb +2 -2
  24. data/lib/ruvim/completion_manager.rb +708 -0
  25. data/lib/ruvim/dispatcher.rb +14 -8
  26. data/lib/ruvim/display_width.rb +91 -45
  27. data/lib/ruvim/editor.rb +74 -80
  28. data/lib/ruvim/ex_command_registry.rb +3 -1
  29. data/lib/ruvim/file_watcher.rb +243 -0
  30. data/lib/ruvim/gh/link.rb +207 -0
  31. data/lib/ruvim/git/blame.rb +255 -0
  32. data/lib/ruvim/git/branch.rb +112 -0
  33. data/lib/ruvim/git/commit.rb +102 -0
  34. data/lib/ruvim/git/diff.rb +129 -0
  35. data/lib/ruvim/git/grep.rb +107 -0
  36. data/lib/ruvim/git/handler.rb +125 -0
  37. data/lib/ruvim/git/log.rb +41 -0
  38. data/lib/ruvim/git/status.rb +103 -0
  39. data/lib/ruvim/global_commands.rb +351 -77
  40. data/lib/ruvim/highlighter.rb +4 -11
  41. data/lib/ruvim/input.rb +1 -0
  42. data/lib/ruvim/key_handler.rb +1510 -0
  43. data/lib/ruvim/keymap_manager.rb +7 -7
  44. data/lib/ruvim/lang/base.rb +5 -0
  45. data/lib/ruvim/lang/c.rb +116 -0
  46. data/lib/ruvim/lang/cpp.rb +107 -0
  47. data/lib/ruvim/lang/csv.rb +4 -1
  48. data/lib/ruvim/lang/diff.rb +43 -0
  49. data/lib/ruvim/lang/dockerfile.rb +36 -0
  50. data/lib/ruvim/lang/elixir.rb +85 -0
  51. data/lib/ruvim/lang/erb.rb +30 -0
  52. data/lib/ruvim/lang/go.rb +83 -0
  53. data/lib/ruvim/lang/html.rb +34 -0
  54. data/lib/ruvim/lang/javascript.rb +83 -0
  55. data/lib/ruvim/lang/json.rb +40 -0
  56. data/lib/ruvim/lang/lua.rb +76 -0
  57. data/lib/ruvim/lang/makefile.rb +36 -0
  58. data/lib/ruvim/lang/markdown.rb +3 -4
  59. data/lib/ruvim/lang/ocaml.rb +77 -0
  60. data/lib/ruvim/lang/perl.rb +91 -0
  61. data/lib/ruvim/lang/python.rb +85 -0
  62. data/lib/ruvim/lang/registry.rb +102 -0
  63. data/lib/ruvim/lang/ruby.rb +7 -0
  64. data/lib/ruvim/lang/rust.rb +95 -0
  65. data/lib/ruvim/lang/scheme.rb +5 -0
  66. data/lib/ruvim/lang/sh.rb +76 -0
  67. data/lib/ruvim/lang/sql.rb +52 -0
  68. data/lib/ruvim/lang/toml.rb +36 -0
  69. data/lib/ruvim/lang/tsv.rb +4 -1
  70. data/lib/ruvim/lang/typescript.rb +53 -0
  71. data/lib/ruvim/lang/yaml.rb +62 -0
  72. data/lib/ruvim/rich_view/json_renderer.rb +131 -0
  73. data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
  74. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  75. data/lib/ruvim/rich_view.rb +30 -7
  76. data/lib/ruvim/screen.rb +135 -84
  77. data/lib/ruvim/stream/file_load.rb +85 -0
  78. data/lib/ruvim/stream/follow.rb +40 -0
  79. data/lib/ruvim/stream/git.rb +43 -0
  80. data/lib/ruvim/stream/run.rb +74 -0
  81. data/lib/ruvim/stream/stdin.rb +55 -0
  82. data/lib/ruvim/stream.rb +35 -0
  83. data/lib/ruvim/stream_mixer.rb +394 -0
  84. data/lib/ruvim/terminal.rb +18 -4
  85. data/lib/ruvim/text_metrics.rb +84 -65
  86. data/lib/ruvim/version.rb +1 -1
  87. data/lib/ruvim/window.rb +5 -5
  88. data/lib/ruvim.rb +31 -4
  89. data/test/app_command_test.rb +382 -0
  90. data/test/app_completion_test.rb +65 -16
  91. data/test/app_dot_repeat_test.rb +27 -3
  92. data/test/app_ex_command_test.rb +154 -0
  93. data/test/app_motion_test.rb +13 -12
  94. data/test/app_register_test.rb +2 -1
  95. data/test/app_scenario_test.rb +182 -8
  96. data/test/app_startup_test.rb +70 -27
  97. data/test/app_text_object_test.rb +2 -1
  98. data/test/app_unicode_behavior_test.rb +3 -2
  99. data/test/browser_test.rb +88 -0
  100. data/test/buffer_test.rb +24 -0
  101. data/test/cli_test.rb +77 -0
  102. data/test/clipboard_test.rb +67 -0
  103. data/test/command_invocation_test.rb +33 -0
  104. data/test/command_line_test.rb +118 -0
  105. data/test/config_dsl_test.rb +134 -0
  106. data/test/dispatcher_test.rb +74 -4
  107. data/test/display_width_test.rb +41 -0
  108. data/test/ex_command_registry_test.rb +106 -0
  109. data/test/file_watcher_test.rb +197 -0
  110. data/test/follow_test.rb +198 -0
  111. data/test/gh_link_test.rb +141 -0
  112. data/test/git_blame_test.rb +792 -0
  113. data/test/git_grep_test.rb +64 -0
  114. data/test/highlighter_test.rb +169 -0
  115. data/test/indent_test.rb +223 -0
  116. data/test/input_screen_integration_test.rb +1 -1
  117. data/test/keyword_chars_test.rb +85 -0
  118. data/test/lang_test.rb +634 -0
  119. data/test/markdown_renderer_test.rb +5 -5
  120. data/test/on_save_hook_test.rb +12 -8
  121. data/test/render_snapshot_test.rb +78 -0
  122. data/test/rich_view_test.rb +279 -23
  123. data/test/run_command_test.rb +307 -0
  124. data/test/screen_test.rb +68 -5
  125. data/test/search_option_test.rb +19 -0
  126. data/test/stream_test.rb +165 -0
  127. data/test/test_helper.rb +9 -0
  128. data/test/window_test.rb +59 -0
  129. metadata +68 -2
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Rust
6
+ KEYWORDS = %w[
7
+ as async await break const continue crate dyn else enum extern
8
+ false fn for if impl in let loop match mod move mut pub ref
9
+ return self Self static struct super trait true type unsafe use
10
+ where while
11
+ macro_rules
12
+ ].freeze
13
+
14
+ TYPES = %w[
15
+ bool char str
16
+ i8 i16 i32 i64 i128 isize
17
+ u8 u16 u32 u64 u128 usize
18
+ f32 f64
19
+ String Vec Box Rc Arc Option Result HashMap HashSet
20
+ Vec! vec!
21
+ ].freeze
22
+
23
+ ALL_KEYWORDS = (KEYWORDS + TYPES).uniq.freeze
24
+
25
+ KEYWORD_RE = /\b(?:#{ALL_KEYWORDS.map { |k| Regexp.escape(k) }.join("|")})\b/
26
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
27
+ STRING_RAW_RE = /r#*"[^"]*"#*/
28
+ CHAR_RE = /'(?:\\.|[^'\\])'/
29
+ LIFETIME_RE = /'[a-z_]\w*/
30
+ NUMBER_RE = /\b(?:0[xXoObB][\da-fA-F_]+|\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?(?:_?[iu](?:8|16|32|64|128|size)|_?f(?:32|64))?)\b/
31
+ LINE_COMMENT_RE = %r{//.*}
32
+ BLOCK_COMMENT_RE = %r{/\*.*?\*/}
33
+ ATTRIBUTE_RE = /#!?\[[\w:(,)\s"]*\]/
34
+ MACRO_RE = /\b\w+!/
35
+ CONSTANT_RE = /\b[A-Z][A-Z0-9_]{1,}\b/
36
+
37
+ INDENT_OPEN_RE = /[{(\[]\s*(?:\/\/.*)?$/
38
+ INDENT_CLOSE_RE = /\A\s*[}\])]/
39
+
40
+ DEDENT_TRIGGERS = {
41
+ "}" => /\A(\s*)\}/,
42
+ "]" => /\A(\s*)\]/,
43
+ ")" => /\A(\s*)\)/
44
+ }.freeze
45
+
46
+ module_function
47
+
48
+ def calculate_indent(lines, target_row, shiftwidth)
49
+ depth = 0
50
+ (0...target_row).each do |row|
51
+ line = lines[row].to_s
52
+ line.each_char do |ch|
53
+ case ch
54
+ when "{", "[", "(" then depth += 1
55
+ when "}", "]", ")" then depth -= 1
56
+ end
57
+ end
58
+ end
59
+
60
+ target_line = lines[target_row].to_s.lstrip
61
+ depth -= 1 if target_line.match?(INDENT_CLOSE_RE)
62
+ depth = 0 if depth < 0
63
+ depth * shiftwidth
64
+ end
65
+
66
+ def indent_trigger?(line)
67
+ line.to_s.rstrip.match?(INDENT_OPEN_RE)
68
+ end
69
+
70
+ def dedent_trigger(char)
71
+ DEDENT_TRIGGERS[char]
72
+ end
73
+
74
+ def color_columns(text)
75
+ cols = {}
76
+ Highlighter.apply_regex(cols, text, CHAR_RE, Highlighter::STRING_COLOR)
77
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
78
+ Highlighter.apply_regex(cols, text, STRING_RAW_RE, Highlighter::STRING_COLOR)
79
+ Highlighter.apply_regex(cols, text, LIFETIME_RE, "\e[35m")
80
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
81
+ Highlighter.apply_regex(cols, text, MACRO_RE, "\e[35m")
82
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
83
+ Highlighter.apply_regex(cols, text, CONSTANT_RE, Highlighter::CONSTANT_COLOR)
84
+ Highlighter.apply_regex(cols, text, ATTRIBUTE_RE, "\e[35m")
85
+ Highlighter.apply_regex(cols, text, BLOCK_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
86
+ Highlighter.apply_regex(cols, text, LINE_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
87
+ cols
88
+ end
89
+ end
90
+
91
+ Registry.register("rust", mod: Rust,
92
+ extensions: %w[.rs],
93
+ runprg: "rustc -o /tmp/a.out % && /tmp/a.out")
94
+ end
95
+ end
@@ -40,5 +40,10 @@ module RuVim
40
40
  cols
41
41
  end
42
42
  end
43
+
44
+ Registry.register("scheme", mod: Scheme,
45
+ extensions: %w[.scm .ss .sld],
46
+ shebangs: %w[gosh],
47
+ runprg: "gosh %")
43
48
  end
44
49
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Sh
6
+ KEYWORDS = %w[
7
+ if then else elif fi case esac for while until do done
8
+ in function select return exit break continue
9
+ local export readonly declare typeset unset
10
+ source eval exec trap set shopt
11
+ ].freeze
12
+
13
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
14
+ STRING_SINGLE_RE = /'[^']*'/
15
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
16
+ VARIABLE_RE = /\$\{?[A-Za-z_]\w*\}?|\$[0-9@#?!\-*$]/
17
+ NUMBER_RE = /\b\d+\b/
18
+ COMMENT_RE = /#.*/
19
+ SHEBANG_RE = /\A#!.*/
20
+ OPERATOR_RE = /(?:\|\||&&|;;|<<|>>)/
21
+
22
+ INDENT_OPEN_RE = /\b(?:then|else|do)\s*$/
23
+ INDENT_CLOSE_RE = /\A\s*(?:fi|done|esac|else|elif)\b/
24
+
25
+ DEDENT_TRIGGERS = {
26
+ "i" => /\A(\s*)fi\z/,
27
+ "e" => /\A(\s*)(?:done|else)\z/,
28
+ "c" => /\A(\s*)esac\z/,
29
+ "f" => /\A(\s*)elif\z/
30
+ }.freeze
31
+
32
+ module_function
33
+
34
+ def calculate_indent(lines, target_row, shiftwidth)
35
+ depth = 0
36
+ (0...target_row).each do |row|
37
+ line = lines[row].to_s.strip
38
+ next if line.empty? || line.start_with?("#")
39
+
40
+ depth += 1 if line.match?(/\b(?:then|else|elif|do)\s*$/) || line.match?(/\{\s*$/)
41
+ depth -= 1 if line.match?(/\A(?:fi|done|esac)\b/) || line.match?(/\A\}/)
42
+ end
43
+
44
+ target_line = lines[target_row].to_s.strip
45
+ depth -= 1 if target_line.match?(/\A(?:fi|done|esac|else|elif)\b/) || target_line.match?(/\A\}/)
46
+ depth = 0 if depth < 0
47
+ depth * shiftwidth
48
+ end
49
+
50
+ def indent_trigger?(line)
51
+ stripped = line.to_s.rstrip
52
+ stripped.match?(/\b(?:then|else|elif|do)\s*$/) || stripped.match?(/\{\s*$/)
53
+ end
54
+
55
+ def dedent_trigger(char)
56
+ DEDENT_TRIGGERS[char]
57
+ end
58
+
59
+ def color_columns(text)
60
+ cols = {}
61
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
62
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
63
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
64
+ Highlighter.apply_regex(cols, text, VARIABLE_RE, Highlighter::VARIABLE_COLOR)
65
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
66
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
67
+ cols
68
+ end
69
+ end
70
+
71
+ Registry.register("sh", mod: Sh,
72
+ extensions: %w[.sh .bash .zsh],
73
+ shebangs: %w[bash sh zsh ksh dash],
74
+ runprg: "bash %")
75
+ end
76
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Sql
6
+ KEYWORDS = %w[
7
+ SELECT FROM WHERE AND OR NOT IN IS NULL LIKE BETWEEN EXISTS
8
+ INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE ALTER DROP
9
+ INDEX VIEW TRIGGER FUNCTION PROCEDURE
10
+ JOIN INNER LEFT RIGHT OUTER FULL CROSS ON USING
11
+ GROUP BY ORDER ASC DESC HAVING LIMIT OFFSET DISTINCT
12
+ UNION ALL INTERSECT EXCEPT AS CASE WHEN THEN ELSE END
13
+ BEGIN COMMIT ROLLBACK TRANSACTION SAVEPOINT
14
+ PRIMARY KEY FOREIGN REFERENCES UNIQUE CHECK DEFAULT CONSTRAINT
15
+ IF ELSE ELSEIF WHILE LOOP FOR RETURN DECLARE
16
+ INT INTEGER BIGINT SMALLINT TINYINT
17
+ FLOAT DOUBLE DECIMAL NUMERIC REAL
18
+ CHAR VARCHAR TEXT BLOB CLOB BOOLEAN DATE TIME TIMESTAMP
19
+ SERIAL AUTO_INCREMENT IDENTITY
20
+ CASCADE RESTRICT SET GRANT REVOKE WITH RECURSIVE
21
+ COUNT SUM AVG MIN MAX COALESCE NULLIF CAST CONVERT
22
+ EXPLAIN ANALYZE VACUUM REINDEX CLUSTER TRUNCATE
23
+ TRUE FALSE
24
+ ].freeze
25
+
26
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/i
27
+ STRING_SINGLE_RE = /'(?:''|[^'])*'/
28
+ STRING_DOUBLE_RE = /"(?:""|[^"])*"/
29
+ NUMBER_RE = /\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/
30
+ LINE_COMMENT_RE = /--.*/
31
+ BLOCK_COMMENT_RE = %r{/\*.*?\*/}
32
+ PARAMETER_RE = /[:@$]\w+/
33
+
34
+ module_function
35
+
36
+ def color_columns(text)
37
+ cols = {}
38
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
39
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
40
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
41
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
42
+ Highlighter.apply_regex(cols, text, PARAMETER_RE, Highlighter::VARIABLE_COLOR)
43
+ Highlighter.apply_regex(cols, text, BLOCK_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
44
+ Highlighter.apply_regex(cols, text, LINE_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
45
+ cols
46
+ end
47
+ end
48
+
49
+ Registry.register("sql", mod: Sql,
50
+ extensions: %w[.sql])
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Toml
6
+ KEYWORD_RE = /\b(?:true|false)\b/
7
+ TABLE_RE = /\A\s*\[[\w.\-"]+\]/
8
+ ARRAY_TABLE_RE = /\A\s*\[\[[\w.\-"]+\]\]/
9
+ KEY_RE = /\A\s*[\w.\-]+\s*(?==)/
10
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
11
+ STRING_SINGLE_RE = /'[^']*'/
12
+ NUMBER_RE = /(?<=[= ])[+-]?(?:0[xXoObB][\da-fA-F_]+|\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?)\b/
13
+ DATETIME_RE = /\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)?/
14
+ COMMENT_RE = /#.*/
15
+
16
+ module_function
17
+
18
+ def color_columns(text)
19
+ cols = {}
20
+ Highlighter.apply_regex(cols, text, ARRAY_TABLE_RE, "\e[1;36m")
21
+ Highlighter.apply_regex(cols, text, TABLE_RE, "\e[1;36m")
22
+ Highlighter.apply_regex(cols, text, KEY_RE, Highlighter::KEYWORD_COLOR)
23
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
24
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
25
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, "\e[35m")
26
+ Highlighter.apply_regex(cols, text, DATETIME_RE, Highlighter::CONSTANT_COLOR)
27
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
28
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
29
+ cols
30
+ end
31
+ end
32
+
33
+ Registry.register("toml", mod: Toml,
34
+ extensions: %w[.toml])
35
+ end
36
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module RuVim
4
4
  module Lang
5
+ Registry.register("tsv", mod: Base,
6
+ extensions: %w[.tsv])
7
+
5
8
  module Tsv
6
9
  module_function
7
10
 
@@ -15,5 +18,5 @@ module RuVim
15
18
  end
16
19
  end
17
20
 
18
- RichView.register("tsv", RichView::TableRenderer, detector: Lang::Tsv.method(:detect?))
21
+ RichView.register(:tsv, RichView::TableRenderer, detector: Lang::Tsv.method(:detect?))
19
22
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Typescript
6
+ TS_KEYWORDS = %w[
7
+ abstract as declare enum implements interface
8
+ module namespace private protected public readonly
9
+ type keyof infer extends
10
+ never unknown any
11
+ override satisfies
12
+ ].freeze
13
+
14
+ ALL_KEYWORDS = (Javascript::KEYWORDS + TS_KEYWORDS).uniq.freeze
15
+
16
+ KEYWORD_RE = /\b(?:#{ALL_KEYWORDS.join("|")})\b/
17
+
18
+ module_function
19
+
20
+ def calculate_indent(lines, target_row, shiftwidth)
21
+ Javascript.calculate_indent(lines, target_row, shiftwidth)
22
+ end
23
+
24
+ def indent_trigger?(line)
25
+ Javascript.indent_trigger?(line)
26
+ end
27
+
28
+ def dedent_trigger(char)
29
+ Javascript.dedent_trigger(char)
30
+ end
31
+
32
+ def color_columns(text)
33
+ cols = {}
34
+ Highlighter.apply_regex(cols, text, Javascript::STRING_SINGLE_RE, Highlighter::STRING_COLOR)
35
+ Highlighter.apply_regex(cols, text, Javascript::STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
36
+ Highlighter.apply_regex(cols, text, Javascript::TEMPLATE_RE, Highlighter::STRING_COLOR)
37
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
38
+ Highlighter.apply_regex(cols, text, Javascript::NUMBER_RE, Highlighter::NUMBER_COLOR)
39
+ Highlighter.apply_regex(cols, text, Javascript::CONSTANT_RE, Highlighter::CONSTANT_COLOR)
40
+ Highlighter.apply_regex(cols, text, Javascript::BLOCK_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
41
+ Highlighter.apply_regex(cols, text, Javascript::LINE_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
42
+ cols
43
+ end
44
+ end
45
+
46
+ Registry.register("typescript", mod: Typescript,
47
+ extensions: %w[.ts],
48
+ aliases: %w[typescriptreact],
49
+ runprg: "npx tsx %")
50
+ Registry.register("typescriptreact", mod: Typescript,
51
+ extensions: %w[.tsx])
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Yaml
6
+ KEYWORDS = %w[true false null yes no on off].freeze
7
+
8
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
9
+ KEY_RE = /\A\s*[\w.\-\/]+\s*(?=:)/
10
+ TAG_RE = /![\w!.\/\-]*/
11
+ ANCHOR_RE = /[&*]\w+/
12
+ COMMENT_RE = /#.*/
13
+ STRING_SINGLE_RE = /'(?:[^'\\]|\\.)*'/
14
+ STRING_DOUBLE_RE = /"(?:[^"\\]|\\.)*"/
15
+ NUMBER_RE = /(?<=[\s:\-\[,]|^)-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\s*$/
16
+ BLOCK_SCALAR_RE = /\A\s*[|>][+-]?\s*$/
17
+
18
+ module_function
19
+
20
+ def calculate_indent(lines, target_row, shiftwidth)
21
+ return 0 if target_row == 0
22
+
23
+ prev_row = target_row - 1
24
+ prev_row -= 1 while prev_row > 0 && lines[prev_row].to_s.strip.empty?
25
+ prev = lines[prev_row].to_s
26
+ prev_indent = prev[/\A */].size
27
+
28
+ # Increase indent after mapping key or list with sub-items
29
+ if prev.match?(/:\s*$/) || prev.match?(/[|>][+-]?\s*$/)
30
+ return prev_indent + shiftwidth
31
+ end
32
+
33
+ prev_indent
34
+ end
35
+
36
+ def indent_trigger?(line)
37
+ line.to_s.rstrip.match?(/:\s*$/) || line.to_s.rstrip.match?(/[|>][+-]?\s*$/)
38
+ end
39
+
40
+ def dedent_trigger(_char)
41
+ nil
42
+ end
43
+
44
+ def color_columns(text)
45
+ cols = {}
46
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
47
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
48
+ Highlighter.apply_regex(cols, text, KEY_RE, Highlighter::KEYWORD_COLOR)
49
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, "\e[35m")
50
+ Highlighter.apply_regex(cols, text, TAG_RE, Highlighter::CONSTANT_COLOR)
51
+ Highlighter.apply_regex(cols, text, ANCHOR_RE, Highlighter::VARIABLE_COLOR)
52
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
53
+ Highlighter.apply_regex(cols, text, BLOCK_SCALAR_RE, Highlighter::STRING_COLOR)
54
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
55
+ cols
56
+ end
57
+ end
58
+
59
+ Registry.register("yaml", mod: Yaml,
60
+ extensions: %w[.yml .yaml])
61
+ end
62
+ 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
@@ -9,9 +9,9 @@ module RuVim
9
9
  module_function
10
10
 
11
11
  def delimiter_for(format)
12
- case format.to_s
13
- when "csv" then ","
14
- when "tsv" then "\t"
12
+ case format
13
+ when :csv then ","
14
+ when :tsv then "\t"
15
15
  else "\t"
16
16
  end
17
17
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "rich_view/table_renderer"
4
4
  require_relative "rich_view/markdown_renderer"
5
+ require_relative "rich_view/json_renderer"
6
+ require_relative "rich_view/jsonl_renderer"
5
7
 
6
8
  module RuVim
7
9
  module RichView
@@ -11,12 +13,12 @@ module RuVim
11
13
  module_function
12
14
 
13
15
  def register(filetype, renderer, detector: nil)
14
- @renderers[filetype.to_s] = renderer
15
- @detectors << { filetype: filetype.to_s, detector: detector } if detector
16
+ @renderers[filetype.to_sym] = renderer
17
+ @detectors << { filetype: filetype.to_sym, detector: detector } if detector
16
18
  end
17
19
 
18
20
  def renderer_for(filetype)
19
- @renderers[filetype.to_s]
21
+ @renderers[filetype.to_sym]
20
22
  end
21
23
 
22
24
  def registered_filetypes
@@ -24,10 +26,13 @@ module RuVim
24
26
  end
25
27
 
26
28
  # Detect format from filetype or buffer content.
27
- # Returns a filetype string ("tsv", "csv", "markdown") or nil.
29
+ # Returns a filetype symbol (:tsv, :csv, :markdown) or nil.
28
30
  def detect_format(buffer)
29
- ft = buffer.options["filetype"].to_s
30
- return ft if @renderers.key?(ft)
31
+ raw = buffer.options["filetype"]
32
+ if raw && !raw.to_s.empty?
33
+ ft = raw.to_sym
34
+ return ft if @renderers.key?(ft)
35
+ end
31
36
 
32
37
  # Ask registered detectors
33
38
  @detectors.each do |entry|
@@ -40,12 +45,17 @@ module RuVim
40
45
  # Enter rich mode on the current buffer (same buffer, no virtual buffer).
41
46
  def open!(editor, format: nil)
42
47
  buffer = editor.current_buffer
43
- format ||= detect_format(buffer)
48
+ format = format ? format.to_sym : detect_format(buffer)
44
49
  raise RuVim::CommandError, "Cannot detect format for rich view" unless format
45
50
 
46
51
  renderer = @renderers[format]
47
52
  raise RuVim::CommandError, "No renderer for format: #{format}" unless renderer
48
53
 
54
+ if renderer.respond_to?(:open_view!)
55
+ renderer.open_view!(editor)
56
+ return
57
+ end
58
+
49
59
  delimiter = renderer.delimiter_for(format)
50
60
  editor.enter_rich_mode(format: format, delimiter: delimiter)
51
61
  editor.echo("[Rich: #{format}]")
@@ -89,5 +99,18 @@ module RuVim
89
99
 
90
100
  editor.exit_rich_mode
91
101
  end
102
+
103
+ # Bind Esc and C-c to close a virtual buffer created by a renderer.
104
+ def bind_close_keys(editor, buffer_id)
105
+ km = editor.keymap_manager
106
+ return unless km
107
+
108
+ km.bind_buffer(buffer_id, "\e", "rich.close_buffer")
109
+ km.bind_buffer(buffer_id, "<C-c>", "rich.close_buffer")
110
+ end
111
+
112
+ register(:markdown, MarkdownRenderer)
113
+ register(:json, JsonRenderer)
114
+ register(:jsonl, JsonlRenderer)
92
115
  end
93
116
  end