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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +53 -4
  3. data/README.md +15 -6
  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 +3 -2
  10. data/docs/command.md +81 -9
  11. data/docs/done.md +23 -0
  12. data/docs/spec.md +105 -19
  13. data/docs/todo.md +9 -0
  14. data/docs/tutorial.md +9 -1
  15. data/docs/vim_diff.md +13 -0
  16. data/ext/ruvim/extconf.rb +5 -0
  17. data/ext/ruvim/ruvim_ext.c +519 -0
  18. data/lib/ruvim/app.rb +217 -2778
  19. data/lib/ruvim/browser.rb +104 -0
  20. data/lib/ruvim/buffer.rb +39 -28
  21. data/lib/ruvim/command_invocation.rb +2 -2
  22. data/lib/ruvim/completion_manager.rb +708 -0
  23. data/lib/ruvim/dispatcher.rb +14 -8
  24. data/lib/ruvim/display_width.rb +91 -45
  25. data/lib/ruvim/editor.rb +64 -81
  26. data/lib/ruvim/ex_command_registry.rb +3 -1
  27. data/lib/ruvim/gh/link.rb +207 -0
  28. data/lib/ruvim/git/blame.rb +16 -6
  29. data/lib/ruvim/git/branch.rb +20 -5
  30. data/lib/ruvim/git/grep.rb +107 -0
  31. data/lib/ruvim/git/handler.rb +42 -1
  32. data/lib/ruvim/global_commands.rb +175 -35
  33. data/lib/ruvim/highlighter.rb +4 -13
  34. data/lib/ruvim/key_handler.rb +1510 -0
  35. data/lib/ruvim/keymap_manager.rb +7 -7
  36. data/lib/ruvim/lang/base.rb +5 -0
  37. data/lib/ruvim/lang/c.rb +116 -0
  38. data/lib/ruvim/lang/cpp.rb +107 -0
  39. data/lib/ruvim/lang/csv.rb +4 -1
  40. data/lib/ruvim/lang/diff.rb +2 -0
  41. data/lib/ruvim/lang/dockerfile.rb +36 -0
  42. data/lib/ruvim/lang/elixir.rb +85 -0
  43. data/lib/ruvim/lang/erb.rb +30 -0
  44. data/lib/ruvim/lang/go.rb +83 -0
  45. data/lib/ruvim/lang/html.rb +34 -0
  46. data/lib/ruvim/lang/javascript.rb +83 -0
  47. data/lib/ruvim/lang/json.rb +6 -0
  48. data/lib/ruvim/lang/lua.rb +76 -0
  49. data/lib/ruvim/lang/makefile.rb +36 -0
  50. data/lib/ruvim/lang/markdown.rb +3 -4
  51. data/lib/ruvim/lang/ocaml.rb +77 -0
  52. data/lib/ruvim/lang/perl.rb +91 -0
  53. data/lib/ruvim/lang/python.rb +85 -0
  54. data/lib/ruvim/lang/registry.rb +102 -0
  55. data/lib/ruvim/lang/ruby.rb +7 -0
  56. data/lib/ruvim/lang/rust.rb +95 -0
  57. data/lib/ruvim/lang/scheme.rb +5 -0
  58. data/lib/ruvim/lang/sh.rb +76 -0
  59. data/lib/ruvim/lang/sql.rb +52 -0
  60. data/lib/ruvim/lang/toml.rb +36 -0
  61. data/lib/ruvim/lang/tsv.rb +4 -1
  62. data/lib/ruvim/lang/typescript.rb +53 -0
  63. data/lib/ruvim/lang/yaml.rb +62 -0
  64. data/lib/ruvim/rich_view/table_renderer.rb +3 -3
  65. data/lib/ruvim/rich_view.rb +14 -7
  66. data/lib/ruvim/screen.rb +126 -72
  67. data/lib/ruvim/stream/file_load.rb +85 -0
  68. data/lib/ruvim/stream/follow.rb +40 -0
  69. data/lib/ruvim/stream/git.rb +43 -0
  70. data/lib/ruvim/stream/run.rb +74 -0
  71. data/lib/ruvim/stream/stdin.rb +55 -0
  72. data/lib/ruvim/stream.rb +35 -0
  73. data/lib/ruvim/stream_mixer.rb +394 -0
  74. data/lib/ruvim/terminal.rb +18 -4
  75. data/lib/ruvim/text_metrics.rb +84 -65
  76. data/lib/ruvim/version.rb +1 -1
  77. data/lib/ruvim/window.rb +5 -5
  78. data/lib/ruvim.rb +23 -6
  79. data/test/app_command_test.rb +382 -0
  80. data/test/app_completion_test.rb +43 -19
  81. data/test/app_dot_repeat_test.rb +27 -3
  82. data/test/app_ex_command_test.rb +154 -0
  83. data/test/app_motion_test.rb +13 -12
  84. data/test/app_register_test.rb +2 -1
  85. data/test/app_scenario_test.rb +15 -10
  86. data/test/app_startup_test.rb +70 -27
  87. data/test/app_text_object_test.rb +2 -1
  88. data/test/app_unicode_behavior_test.rb +3 -2
  89. data/test/browser_test.rb +88 -0
  90. data/test/buffer_test.rb +24 -0
  91. data/test/cli_test.rb +63 -0
  92. data/test/command_invocation_test.rb +33 -0
  93. data/test/config_dsl_test.rb +47 -0
  94. data/test/dispatcher_test.rb +74 -4
  95. data/test/ex_command_registry_test.rb +106 -0
  96. data/test/follow_test.rb +20 -21
  97. data/test/gh_link_test.rb +141 -0
  98. data/test/git_blame_test.rb +96 -17
  99. data/test/git_grep_test.rb +64 -0
  100. data/test/highlighter_test.rb +125 -0
  101. data/test/indent_test.rb +137 -0
  102. data/test/input_screen_integration_test.rb +1 -1
  103. data/test/keyword_chars_test.rb +85 -0
  104. data/test/lang_test.rb +634 -0
  105. data/test/markdown_renderer_test.rb +5 -5
  106. data/test/on_save_hook_test.rb +12 -8
  107. data/test/render_snapshot_test.rb +78 -0
  108. data/test/rich_view_test.rb +42 -42
  109. data/test/run_command_test.rb +307 -0
  110. data/test/screen_test.rb +68 -5
  111. data/test/stream_test.rb +165 -0
  112. data/test/window_test.rb +59 -0
  113. metadata +52 -2
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Lua
6
+ KEYWORDS = %w[
7
+ and break do else elseif end false for function goto if in
8
+ local nil not or repeat return then true until while
9
+ ].freeze
10
+
11
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
12
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
13
+ STRING_SINGLE_RE = /'(?:\\.|[^'\\])*'/
14
+ LONG_STRING_RE = /\[\[.*?\]\]/
15
+ NUMBER_RE = /\b(?:0[xX][\da-fA-F]+(?:\.[\da-fA-F]+)?(?:[pP][+-]?\d+)?|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/
16
+ LINE_COMMENT_RE = /--(?!\[\[).*/
17
+ BLOCK_COMMENT_RE = /--\[\[.*?\]\]/
18
+ BUILTIN_RE = /\b(?:print|type|tostring|tonumber|pairs|ipairs|next|select|unpack|require|error|assert|pcall|xpcall|setmetatable|getmetatable|rawget|rawset|rawequal|rawlen|table|string|math|io|os|coroutine|debug|package)\b/
19
+
20
+ INDENT_OPEN_RE = /\b(?:function|if|for|while|repeat|do|else|elseif)\b/
21
+ INDENT_CLOSE_RE = /\A\s*(?:end|until)\b/
22
+ INDENT_MID_RE = /\A\s*(?:else|elseif)\b/
23
+
24
+ DEDENT_TRIGGERS = {
25
+ "d" => /\A(\s*)end\z/,
26
+ "l" => /\A(\s*)until\z/,
27
+ "e" => /\A(\s*)(?:else|elseif)\z/
28
+ }.freeze
29
+
30
+ module_function
31
+
32
+ def calculate_indent(lines, target_row, shiftwidth)
33
+ depth = 0
34
+ (0...target_row).each do |row|
35
+ line = lines[row].to_s.strip
36
+ next if line.empty? || line.start_with?("--")
37
+
38
+ depth += 1 if line.match?(/\b(?:function|if|for|while|repeat|do)\b/) && !line.match?(/\bend\b/)
39
+ depth -= 1 if line.match?(/\A(?:end|until)\b/)
40
+ end
41
+
42
+ target_line = lines[target_row].to_s.strip
43
+ depth -= 1 if target_line.match?(INDENT_CLOSE_RE) || target_line.match?(INDENT_MID_RE)
44
+ depth = 0 if depth < 0
45
+ depth * shiftwidth
46
+ end
47
+
48
+ def indent_trigger?(line)
49
+ stripped = line.to_s.rstrip
50
+ stripped.match?(/\b(?:function|if|for|while|repeat|do|then|else)\b/)
51
+ end
52
+
53
+ def dedent_trigger(char)
54
+ DEDENT_TRIGGERS[char]
55
+ end
56
+
57
+ def color_columns(text)
58
+ cols = {}
59
+ Highlighter.apply_regex(cols, text, LONG_STRING_RE, Highlighter::STRING_COLOR)
60
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
61
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
62
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
63
+ Highlighter.apply_regex(cols, text, BUILTIN_RE, "\e[35m")
64
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
65
+ Highlighter.apply_regex(cols, text, BLOCK_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
66
+ Highlighter.apply_regex(cols, text, LINE_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
67
+ cols
68
+ end
69
+ end
70
+
71
+ Registry.register("lua", mod: Lua,
72
+ extensions: %w[.lua],
73
+ shebangs: ["lua", /\Alua\d*\z/],
74
+ runprg: "lua %")
75
+ end
76
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Makefile
6
+ TARGET_RE = /\A[\w.\-\/]+\s*:/
7
+ VARIABLE_DEF_RE = /\A[\w.\-]+\s*[?:+]?=/
8
+ VARIABLE_REF_RE = /\$[({][\w.\-]+[)}]|\$[A-Za-z@<*^?%]/
9
+ DIRECTIVE_RE = /\A\s*(?:ifeq|ifneq|ifdef|ifndef|else|endif|define|endef|include|-include|sinclude|override|export|unexport|vpath)\b/
10
+ FUNCTION_RE = /\$\((?:subst|patsubst|strip|findstring|filter|filter-out|sort|word|wordlist|words|firstword|lastword|dir|notdir|suffix|basename|addsuffix|addprefix|join|wildcard|realpath|abspath|foreach|if|or|and|call|eval|file|value|error|warning|info|shell|origin|flavor|guile)\b/
11
+ COMMENT_RE = /#.*/
12
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
13
+ STRING_SINGLE_RE = /'(?:\\.|[^'\\])*'/
14
+ AUTO_VAR_RE = /\$[@<*^?%]/
15
+
16
+ module_function
17
+
18
+ def color_columns(text)
19
+ cols = {}
20
+ Highlighter.apply_regex(cols, text, TARGET_RE, "\e[1;33m")
21
+ Highlighter.apply_regex(cols, text, VARIABLE_DEF_RE, Highlighter::KEYWORD_COLOR)
22
+ Highlighter.apply_regex(cols, text, DIRECTIVE_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, FUNCTION_RE, "\e[35m")
26
+ Highlighter.apply_regex(cols, text, VARIABLE_REF_RE, Highlighter::VARIABLE_COLOR)
27
+ Highlighter.apply_regex(cols, text, AUTO_VAR_RE, Highlighter::VARIABLE_COLOR)
28
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
29
+ cols
30
+ end
31
+ end
32
+
33
+ Registry.register("make", mod: Makefile,
34
+ basenames: %w[Makefile GNUmakefile makefile Justfile])
35
+ end
36
+ end
@@ -80,10 +80,6 @@ module RuVim
80
80
 
81
81
  module_function
82
82
 
83
- def heading?(line)
84
- line.to_s.match?(HEADING_RE)
85
- end
86
-
87
83
  def heading_level(line)
88
84
  m = line.to_s.match(HEADING_RE)
89
85
  m ? m[2].length : 0
@@ -166,5 +162,8 @@ module RuVim
166
162
  text.length.times { |i| cols[i] = color }
167
163
  end
168
164
  end
165
+
166
+ Registry.register("markdown", mod: Markdown,
167
+ extensions: %w[.md])
169
168
  end
170
169
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Ocaml
6
+ KEYWORDS = %w[
7
+ and as assert begin class constraint do done downto else end
8
+ exception external false for fun function functor if in include
9
+ inherit initializer lazy let match method mod module mutable
10
+ new nonrec object of open or private rec sig struct then to
11
+ true try type val virtual when while with
12
+ ].freeze
13
+
14
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
15
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
16
+ CHAR_RE = /'(?:\\.|[^'\\])'/
17
+ NUMBER_RE = /\b(?:0[xXoObB][\da-fA-F_]+|\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?)\b/
18
+ BLOCK_COMMENT_RE = /\(\*.*?\*\)/
19
+ MODULE_RE = /\b[A-Z]\w*(?:\.[A-Z]\w*)*/
20
+ VARIANT_RE = /\b[A-Z]\w*\b/
21
+ TYPE_VAR_RE = /'\w+/
22
+ OPERATOR_RE = /->|::|;;|<-|\|>/
23
+
24
+ INDENT_OPEN_RE = /\b(?:struct|sig|begin|do|then|else)\s*$/
25
+ INDENT_CLOSE_RE = /\A\s*(?:end|done)\b/
26
+
27
+ DEDENT_TRIGGERS = {
28
+ "d" => /\A(\s*)end\z/,
29
+ "e" => /\A(\s*)(?:done|else)\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?
39
+
40
+ depth += 1 if line.match?(/\b(?:struct|sig|begin|do)\b/)
41
+ depth -= 1 if line.match?(/\A(?:end|done)\b/)
42
+ end
43
+
44
+ target_line = lines[target_row].to_s.strip
45
+ depth -= 1 if target_line.match?(INDENT_CLOSE_RE)
46
+ depth = 0 if depth < 0
47
+ depth * shiftwidth
48
+ end
49
+
50
+ def indent_trigger?(line)
51
+ line.to_s.rstrip.match?(INDENT_OPEN_RE)
52
+ end
53
+
54
+ def dedent_trigger(char)
55
+ DEDENT_TRIGGERS[char]
56
+ end
57
+
58
+ def color_columns(text)
59
+ cols = {}
60
+ Highlighter.apply_regex(cols, text, CHAR_RE, Highlighter::STRING_COLOR)
61
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
62
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
63
+ Highlighter.apply_regex(cols, text, TYPE_VAR_RE, Highlighter::VARIABLE_COLOR)
64
+ Highlighter.apply_regex(cols, text, MODULE_RE, Highlighter::CONSTANT_COLOR)
65
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
66
+ Highlighter.apply_regex(cols, text, OPERATOR_RE, Highlighter::KEYWORD_COLOR)
67
+ Highlighter.apply_regex(cols, text, BLOCK_COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
68
+ cols
69
+ end
70
+ end
71
+
72
+ Registry.register("ocaml", mod: Ocaml,
73
+ extensions: %w[.ml .mli],
74
+ shebangs: %w[ocaml],
75
+ runprg: "ocaml %")
76
+ end
77
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Perl
6
+ KEYWORDS = %w[
7
+ my our local sub return if elsif else unless while until for
8
+ foreach do given when default last next redo goto
9
+ use require package no BEGIN END
10
+ die warn print say chomp chop push pop shift unshift
11
+ open close read write seek tell
12
+ map grep sort reverse join split
13
+ defined undef delete exists ref bless
14
+ eval try catch finally
15
+ and or not xor
16
+ eq ne lt gt le ge cmp
17
+ ].freeze
18
+
19
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
20
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
21
+ STRING_SINGLE_RE = /'(?:\\.|[^'\\])*'/
22
+ REGEX_RE = %r{(?<=[=~(,;\s])(?:m|s|tr|y)?/(?:\\.|[^/\\])*/[gimxsecpodualn]*}
23
+ QW_RE = /\bqw\s*[({<\/|].*?[)}>\/|]/
24
+ SCALAR_RE = /\$[\w]+/
25
+ ARRAY_RE = /@[\w]+/
26
+ HASH_RE = /%[\w]+/
27
+ SPECIAL_VAR_RE = /\$[_!@&`'+\\\/\-\[\]]/
28
+ NUMBER_RE = /\b(?:0[xXoObB][\da-fA-F_]+|\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?)\b/
29
+ COMMENT_RE = /#.*/
30
+ POD_RE = /\A=[a-zA-Z]\w*/
31
+
32
+ INDENT_OPEN_RE = /\{\s*(?:#.*)?$/
33
+ INDENT_CLOSE_RE = /\A\s*\}/
34
+
35
+ DEDENT_TRIGGERS = {
36
+ "}" => /\A(\s*)\}/
37
+ }.freeze
38
+
39
+ module_function
40
+
41
+ def calculate_indent(lines, target_row, shiftwidth)
42
+ depth = 0
43
+ (0...target_row).each do |row|
44
+ line = lines[row].to_s
45
+ line.each_char do |ch|
46
+ case ch
47
+ when "{" then depth += 1
48
+ when "}" then depth -= 1
49
+ end
50
+ end
51
+ end
52
+
53
+ target_line = lines[target_row].to_s.lstrip
54
+ depth -= 1 if target_line.match?(INDENT_CLOSE_RE)
55
+ depth = 0 if depth < 0
56
+ depth * shiftwidth
57
+ end
58
+
59
+ def indent_trigger?(line)
60
+ line.to_s.rstrip.match?(INDENT_OPEN_RE)
61
+ end
62
+
63
+ def dedent_trigger(char)
64
+ DEDENT_TRIGGERS[char]
65
+ end
66
+
67
+ def color_columns(text)
68
+ cols = {}
69
+ # POD documentation
70
+ if text.match?(POD_RE)
71
+ text.length.times { |i| cols[i] = Highlighter::COMMENT_COLOR }
72
+ return cols
73
+ end
74
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
75
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
76
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
77
+ Highlighter.apply_regex(cols, text, SCALAR_RE, Highlighter::VARIABLE_COLOR)
78
+ Highlighter.apply_regex(cols, text, ARRAY_RE, "\e[35m")
79
+ Highlighter.apply_regex(cols, text, HASH_RE, Highlighter::CONSTANT_COLOR)
80
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
81
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
82
+ cols
83
+ end
84
+ end
85
+
86
+ Registry.register("perl", mod: Perl,
87
+ extensions: %w[.pl .pm .t],
88
+ shebangs: [/\Aperl(?:\d+(?:\.\d+)*)?\z/],
89
+ runprg: "perl %")
90
+ end
91
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ module Python
6
+ KEYWORDS = %w[
7
+ and as assert async await break class continue def del elif else
8
+ except finally for from global if import in is lambda nonlocal
9
+ not or pass raise return try while with yield
10
+ True False None
11
+ ].freeze
12
+
13
+ KEYWORD_RE = /\b(?:#{KEYWORDS.join("|")})\b/
14
+ STRING_TRIPLE_DQ_RE = /""".*?"""/m
15
+ STRING_TRIPLE_SQ_RE = /'''.*?'''/m
16
+ STRING_DOUBLE_RE = /"(?:\\.|[^"\\])*"/
17
+ STRING_SINGLE_RE = /'(?:\\.|[^'\\])*'/
18
+ FSTRING_PREFIX_RE = /[fFrRbBuU]{1,2}(?=["'])/
19
+ NUMBER_RE = /\b(?:0[xXoObB][\da-fA-F_]+|\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?j?)\b/
20
+ DECORATOR_RE = /@[\w.]+/
21
+ COMMENT_RE = /#.*/
22
+ CONSTANT_RE = /\b[A-Z][A-Z0-9_]{1,}\b/
23
+ BUILTIN_RE = /\b(?:print|len|range|type|int|str|float|list|dict|tuple|set|bool|open|input|map|filter|zip|enumerate|sorted|reversed|super|isinstance|issubclass|hasattr|getattr|setattr|delattr|property|staticmethod|classmethod|__\w+__)\b/
24
+
25
+ INDENT_OPEN_RE = /:\s*(?:#.*)?$/
26
+ INDENT_CLOSE_RE = /\A\s*(?:return|break|continue|pass|raise)\b/
27
+
28
+ DEDENT_TRIGGERS = {
29
+ "s" => /\A(\s*)(?:else|class)\z/,
30
+ ":" => /\A(\s*)(?:else|elif|except|finally)\s*.*:\z/,
31
+ "f" => /\A(\s*)elif\z/
32
+ }.freeze
33
+
34
+ module_function
35
+
36
+ def calculate_indent(lines, target_row, shiftwidth)
37
+ return 0 if target_row == 0
38
+
39
+ prev_row = target_row - 1
40
+ prev_row -= 1 while prev_row > 0 && lines[prev_row].to_s.strip.empty?
41
+ prev = lines[prev_row].to_s
42
+ prev_indent = prev[/\A */].size
43
+
44
+ if prev.rstrip.match?(INDENT_OPEN_RE)
45
+ return prev_indent + shiftwidth
46
+ end
47
+
48
+ target_line = lines[target_row].to_s.strip
49
+ if target_line.match?(/\A(?:else|elif|except|finally)\b/)
50
+ depth = prev_indent - shiftwidth
51
+ return depth < 0 ? 0 : depth
52
+ end
53
+
54
+ prev_indent
55
+ end
56
+
57
+ def indent_trigger?(line)
58
+ line.to_s.rstrip.match?(INDENT_OPEN_RE)
59
+ end
60
+
61
+ def dedent_trigger(char)
62
+ DEDENT_TRIGGERS[char]
63
+ end
64
+
65
+ def color_columns(text)
66
+ cols = {}
67
+ Highlighter.apply_regex(cols, text, STRING_DOUBLE_RE, Highlighter::STRING_COLOR)
68
+ Highlighter.apply_regex(cols, text, STRING_SINGLE_RE, Highlighter::STRING_COLOR)
69
+ Highlighter.apply_regex(cols, text, FSTRING_PREFIX_RE, Highlighter::STRING_COLOR)
70
+ Highlighter.apply_regex(cols, text, KEYWORD_RE, Highlighter::KEYWORD_COLOR)
71
+ Highlighter.apply_regex(cols, text, BUILTIN_RE, "\e[35m")
72
+ Highlighter.apply_regex(cols, text, DECORATOR_RE, "\e[35m")
73
+ Highlighter.apply_regex(cols, text, NUMBER_RE, Highlighter::NUMBER_COLOR)
74
+ Highlighter.apply_regex(cols, text, CONSTANT_RE, Highlighter::CONSTANT_COLOR)
75
+ Highlighter.apply_regex(cols, text, COMMENT_RE, Highlighter::COMMENT_COLOR, override: true)
76
+ cols
77
+ end
78
+ end
79
+
80
+ Registry.register("python", mod: Python,
81
+ extensions: %w[.py],
82
+ shebangs: [/\Apython(?:\d+(?:\.\d+)*)?\z/],
83
+ runprg: "python3 %")
84
+ end
85
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuVim
4
+ module Lang
5
+ # Central registry for language modules.
6
+ # Each lang module registers itself at load time via Lang.register.
7
+ module Registry
8
+ @entries = {}
9
+
10
+ class << self
11
+ # Register a language module.
12
+ #
13
+ # @param filetype [String] primary filetype name (e.g. "ruby")
14
+ # @param mod [Module] the lang module (must respond to :color_columns)
15
+ # @param extensions [Array<String>] file extensions including dot (e.g. [".rb", ".rake"])
16
+ # @param basenames [Array<String>] exact basenames (e.g. ["Makefile"])
17
+ # @param basename_prefix [String, nil] prefix match for basename (e.g. "Dockerfile")
18
+ # @param shebangs [Array<String, Regexp>] shebang command matchers
19
+ # @param aliases [Array<String>] additional filetype names that map to the same module
20
+ # @param runprg [String, nil] default run command (% = filename)
21
+ def register(filetype, mod:, extensions: [], basenames: [], basename_prefix: nil,
22
+ shebangs: [], aliases: [], runprg: nil)
23
+ entry = {
24
+ filetype: filetype,
25
+ mod: mod,
26
+ extensions: extensions,
27
+ basenames: basenames,
28
+ basename_prefix: basename_prefix,
29
+ shebangs: shebangs,
30
+ aliases: aliases,
31
+ runprg: runprg
32
+ }.freeze
33
+ @entries[filetype] = entry
34
+ aliases.each { |a| @entries[a] = entry }
35
+ end
36
+
37
+ # Look up a lang module by filetype string.
38
+ # Returns the module or Lang::Base if not found.
39
+ def resolve_module(ft)
40
+ entry = @entries[ft]
41
+ entry ? entry[:mod] : Lang::Base
42
+ end
43
+
44
+ # Look up runprg by filetype string. Returns nil if not registered.
45
+ def runprg_for(ft)
46
+ entry = @entries[ft]
47
+ entry&.[](:runprg)
48
+ end
49
+
50
+ # Detect filetype from file extension.
51
+ # Returns filetype string or nil.
52
+ def detect_by_extension(ext)
53
+ ext = ext.downcase
54
+ @entries.each_value do |entry|
55
+ return entry[:filetype] if entry[:extensions].include?(ext)
56
+ end
57
+ nil
58
+ end
59
+
60
+ # Detect filetype from exact basename.
61
+ # Returns filetype string or nil.
62
+ def detect_by_basename(basename)
63
+ @entries.each_value do |entry|
64
+ return entry[:filetype] if entry[:basenames].include?(basename)
65
+ end
66
+ # Prefix match
67
+ @entries.each_value do |entry|
68
+ prefix = entry[:basename_prefix]
69
+ return entry[:filetype] if prefix && basename.start_with?(prefix)
70
+ end
71
+ nil
72
+ end
73
+
74
+ # Detect filetype from shebang command name.
75
+ # Returns filetype string or nil.
76
+ def detect_by_shebang(cmd)
77
+ @entries.each_value do |entry|
78
+ entry[:shebangs].each do |matcher|
79
+ if matcher.is_a?(Regexp)
80
+ return entry[:filetype] if matcher.match?(cmd)
81
+ elsif matcher.to_s == cmd
82
+ return entry[:filetype]
83
+ end
84
+ end
85
+ end
86
+ nil
87
+ end
88
+
89
+ # Returns true if the filetype has a color_columns method.
90
+ def highlight?(ft)
91
+ entry = @entries[ft]
92
+ entry && entry[:mod].respond_to?(:color_columns)
93
+ end
94
+
95
+ # Look up entry by filetype. Returns nil if not found.
96
+ def [](ft)
97
+ @entries[ft]
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -189,6 +189,7 @@ module RuVim
189
189
 
190
190
  def on_save(ctx, path)
191
191
  return unless path && File.exist?(path)
192
+ return if ctx.editor.respond_to?(:restricted_mode?) && ctx.editor.restricted_mode?
192
193
  output, status = Open3.capture2e("ruby", "-wc", path)
193
194
  message = output.sub(/^Syntax OK\n?\z/m, "").strip
194
195
 
@@ -232,5 +233,11 @@ module RuVim
232
233
  cols
233
234
  end
234
235
  end
236
+
237
+ Registry.register("ruby", mod: Ruby,
238
+ extensions: %w[.rb .rake .ru],
239
+ basenames: %w[Gemfile Rakefile Guardfile Vagrantfile],
240
+ shebangs: [/\Aruby(?:\d+(?:\.\d+)*)?\z/],
241
+ runprg: "ruby -w %")
235
242
  end
236
243
  end
@@ -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