mui 0.1.0 → 0.3.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -0
  3. data/CHANGELOG.md +448 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -0
  7. data/docs/getting-started.md +140 -0
  8. data/docs/index.md +55 -0
  9. data/docs/jobs.md +297 -0
  10. data/docs/keybindings.md +229 -0
  11. data/docs/plugins.md +285 -0
  12. data/docs/syntax-highlighting.md +149 -0
  13. data/exe/mui +1 -2
  14. data/lib/mui/autocmd.rb +66 -0
  15. data/lib/mui/buffer.rb +275 -0
  16. data/lib/mui/buffer_word_cache.rb +131 -0
  17. data/lib/mui/buffer_word_completer.rb +77 -0
  18. data/lib/mui/color_manager.rb +136 -0
  19. data/lib/mui/color_scheme.rb +63 -0
  20. data/lib/mui/command_completer.rb +30 -0
  21. data/lib/mui/command_context.rb +90 -0
  22. data/lib/mui/command_history.rb +89 -0
  23. data/lib/mui/command_line.rb +167 -0
  24. data/lib/mui/command_registry.rb +44 -0
  25. data/lib/mui/completion_renderer.rb +84 -0
  26. data/lib/mui/completion_state.rb +58 -0
  27. data/lib/mui/config.rb +58 -0
  28. data/lib/mui/editor.rb +395 -0
  29. data/lib/mui/error.rb +29 -0
  30. data/lib/mui/file_completer.rb +51 -0
  31. data/lib/mui/floating_window.rb +161 -0
  32. data/lib/mui/handler_result.rb +107 -0
  33. data/lib/mui/highlight.rb +22 -0
  34. data/lib/mui/highlighters/base.rb +23 -0
  35. data/lib/mui/highlighters/search_highlighter.rb +27 -0
  36. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  37. data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
  38. data/lib/mui/input.rb +17 -0
  39. data/lib/mui/insert_completion_renderer.rb +92 -0
  40. data/lib/mui/insert_completion_state.rb +77 -0
  41. data/lib/mui/job.rb +81 -0
  42. data/lib/mui/job_manager.rb +113 -0
  43. data/lib/mui/key_code.rb +30 -0
  44. data/lib/mui/key_handler/base.rb +187 -0
  45. data/lib/mui/key_handler/command_mode.rb +511 -0
  46. data/lib/mui/key_handler/insert_mode.rb +323 -0
  47. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  48. data/lib/mui/key_handler/normal_mode.rb +552 -0
  49. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  50. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  51. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  52. data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
  53. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  54. data/lib/mui/key_handler/search_mode.rb +191 -0
  55. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  56. data/lib/mui/key_handler/visual_mode.rb +402 -0
  57. data/lib/mui/key_handler/window_command.rb +112 -0
  58. data/lib/mui/key_handler.rb +16 -0
  59. data/lib/mui/key_notation_parser.rb +152 -0
  60. data/lib/mui/key_sequence.rb +67 -0
  61. data/lib/mui/key_sequence_buffer.rb +85 -0
  62. data/lib/mui/key_sequence_handler.rb +163 -0
  63. data/lib/mui/key_sequence_matcher.rb +79 -0
  64. data/lib/mui/layout/calculator.rb +15 -0
  65. data/lib/mui/layout/leaf_node.rb +33 -0
  66. data/lib/mui/layout/node.rb +29 -0
  67. data/lib/mui/layout/split_node.rb +132 -0
  68. data/lib/mui/line_renderer.rb +173 -0
  69. data/lib/mui/mode.rb +13 -0
  70. data/lib/mui/mode_manager.rb +186 -0
  71. data/lib/mui/motion.rb +139 -0
  72. data/lib/mui/plugin.rb +35 -0
  73. data/lib/mui/plugin_manager.rb +106 -0
  74. data/lib/mui/register.rb +110 -0
  75. data/lib/mui/screen.rb +103 -0
  76. data/lib/mui/search_completer.rb +50 -0
  77. data/lib/mui/search_input.rb +40 -0
  78. data/lib/mui/search_state.rb +121 -0
  79. data/lib/mui/selection.rb +55 -0
  80. data/lib/mui/status_line_renderer.rb +40 -0
  81. data/lib/mui/syntax/language_detector.rb +106 -0
  82. data/lib/mui/syntax/lexer_base.rb +106 -0
  83. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  84. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  85. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  86. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  87. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  88. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  89. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  90. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  91. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  92. data/lib/mui/syntax/token.rb +42 -0
  93. data/lib/mui/syntax/token_cache.rb +91 -0
  94. data/lib/mui/tab_bar_renderer.rb +87 -0
  95. data/lib/mui/tab_manager.rb +96 -0
  96. data/lib/mui/tab_page.rb +35 -0
  97. data/lib/mui/terminal_adapter/base.rb +92 -0
  98. data/lib/mui/terminal_adapter/curses.rb +164 -0
  99. data/lib/mui/terminal_adapter.rb +4 -0
  100. data/lib/mui/themes/default.rb +315 -0
  101. data/lib/mui/undo_manager.rb +83 -0
  102. data/lib/mui/undoable_action.rb +175 -0
  103. data/lib/mui/unicode_width.rb +100 -0
  104. data/lib/mui/version.rb +1 -1
  105. data/lib/mui/window.rb +201 -0
  106. data/lib/mui/window_manager.rb +256 -0
  107. data/lib/mui/wrap_cache.rb +40 -0
  108. data/lib/mui/wrap_helper.rb +84 -0
  109. data/lib/mui.rb +171 -2
  110. metadata +123 -5
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Syntax
5
+ module Lexers
6
+ # Lexer for HTML source files
7
+ class HtmlLexer < LexerBase
8
+ # Pre-compiled patterns with \G anchor for position-specific matching
9
+ COMPILED_PATTERNS = [
10
+ # HTML comment (single line)
11
+ [:comment, /\G<!--.*?-->/],
12
+ # DOCTYPE declaration
13
+ [:preprocessor, /\G<!DOCTYPE[^>]*>/i],
14
+ # CDATA section
15
+ [:string, /\G<!\[CDATA\[.*?\]\]>/],
16
+ # Closing tag
17
+ [:keyword, %r{\G</[a-zA-Z][a-zA-Z0-9-]*\s*>}],
18
+ # Self-closing tag
19
+ [:keyword, %r{\G<[a-zA-Z][a-zA-Z0-9-]*(?:\s+[^>]*)?/>}],
20
+ # Opening tag with attributes
21
+ [:keyword, /\G<[a-zA-Z][a-zA-Z0-9-]*(?=[\s>])/],
22
+ # Tag closing bracket
23
+ [:keyword, /\G>/],
24
+ # Attribute name
25
+ [:type, /\G[a-zA-Z][a-zA-Z0-9_-]*(?==)/],
26
+ # Double quoted attribute value
27
+ [:string, /\G"[^"]*"/],
28
+ # Single quoted attribute value
29
+ [:string, /\G'[^']*'/],
30
+ # Unquoted attribute value (limited characters)
31
+ [:string, /\G=[^\s>"']+/],
32
+ # HTML entities
33
+ [:constant, /\G&(?:#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/],
34
+ # Equal sign (for attributes)
35
+ [:operator, /\G=/]
36
+ ].freeze
37
+
38
+ # Multiline comment patterns
39
+ COMMENT_START = /<!--/
40
+ COMMENT_END = /-->/
41
+ COMMENT_START_ANCHOR = /\G<!--/
42
+
43
+ protected
44
+
45
+ def compiled_patterns
46
+ COMPILED_PATTERNS
47
+ end
48
+
49
+ # Handle multiline HTML comments
50
+ def handle_multiline_state(line, pos, state)
51
+ return [nil, nil, pos] unless state == :html_comment
52
+
53
+ end_match = line[pos..].match(COMMENT_END)
54
+ if end_match
55
+ end_pos = pos + end_match.begin(0) + 2 # --> is 3 chars
56
+ text = line[pos..end_pos]
57
+ token = Token.new(
58
+ type: :comment,
59
+ start_col: pos,
60
+ end_col: end_pos,
61
+ text:
62
+ )
63
+ [token, nil, end_pos + 1]
64
+ else
65
+ text = line[pos..]
66
+ token = if text.empty?
67
+ nil
68
+ else
69
+ Token.new(
70
+ type: :comment,
71
+ start_col: pos,
72
+ end_col: line.length - 1,
73
+ text:
74
+ )
75
+ end
76
+ [token, :html_comment, line.length]
77
+ end
78
+ end
79
+
80
+ def check_multiline_start(line, pos)
81
+ rest = line[pos..]
82
+
83
+ # Check for <!-- that doesn't have --> on this line
84
+ start_match = rest.match(COMMENT_START)
85
+ return [nil, nil, pos] unless start_match
86
+
87
+ start_pos = pos + start_match.begin(0)
88
+ after_start = line[(start_pos + 4)..] # Skip <!--
89
+
90
+ if after_start&.include?("-->")
91
+ [nil, nil, pos]
92
+ else
93
+ text = line[start_pos..]
94
+ token = Token.new(
95
+ type: :comment,
96
+ start_col: start_pos,
97
+ end_col: line.length - 1,
98
+ text:
99
+ )
100
+ [:html_comment, token, line.length]
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def match_token(line, pos)
107
+ # Check for start of multiline comment
108
+ if line[pos..].match?(COMMENT_START_ANCHOR)
109
+ rest = line[(pos + 4)..]
110
+ return nil unless rest&.include?("-->")
111
+ end
112
+
113
+ super
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Syntax
5
+ module Lexers
6
+ # Lexer for JavaScript source code
7
+ class JavaScriptLexer < LexerBase
8
+ # JavaScript keywords
9
+ KEYWORDS = %w[
10
+ async await break case catch class const continue debugger default
11
+ delete do else export extends finally for function if import in
12
+ instanceof let new return static switch throw try typeof var void
13
+ while with yield
14
+ ].freeze
15
+
16
+ # JavaScript built-in types and values
17
+ CONSTANTS = %w[true false null undefined NaN Infinity this super].freeze
18
+
19
+ # Pre-compiled patterns with \G anchor for position-specific matching
20
+ COMPILED_PATTERNS = [
21
+ # Single line comment
22
+ [:comment, %r{\G//.*}],
23
+ # Single-line block comment /* ... */ on one line
24
+ [:comment, %r{\G/\*.*?\*/}],
25
+ # Template literal (single line)
26
+ [:string, /\G`[^`]*`/],
27
+ # Double quoted string (with escape handling)
28
+ [:string, /\G"(?:[^"\\]|\\.)*"/],
29
+ # Single quoted string (with escape handling)
30
+ [:string, /\G'(?:[^'\\]|\\.)*'/],
31
+ # Regular expression literal
32
+ [:regex, %r{\G/(?:[^/\\]|\\.)+/[gimsuy]*}],
33
+ # Float numbers (must be before integer)
34
+ [:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
35
+ # Hexadecimal
36
+ [:number, /\G\b0x[0-9a-fA-F]+n?\b/i],
37
+ # Octal
38
+ [:number, /\G\b0o[0-7]+n?\b/i],
39
+ # Binary
40
+ [:number, /\G\b0b[01]+n?\b/i],
41
+ # Integer (with optional BigInt suffix)
42
+ [:number, /\G\b\d+n?\b/],
43
+ # Constants (true, false, null, undefined, NaN, Infinity, this, super)
44
+ [:constant, /\G\b(?:true|false|null|undefined|NaN|Infinity|this|super)\b/],
45
+ # Keywords
46
+ [:keyword, /\G\b(?:async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|export|extends|finally|for|function|if|import|in|instanceof|let|new|return|static|switch|throw|try|typeof|var|void|while|with|yield)\b/],
47
+ # Class/constructor names (start with uppercase)
48
+ [:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
49
+ # Regular identifiers
50
+ [:identifier, /\G\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/],
51
+ # Operators (=>must come before = patterns)
52
+ [:operator, %r{\G(?:\.{3}|=>|&&=?|\|\|=?|\?\?=?|===?|!==?|>>>?=?|<<=?|\+\+|--|\?\.?|[+\-*/%&|^<>=!]=?)}]
53
+ ].freeze
54
+
55
+ # Multiline patterns (pre-compiled)
56
+ BLOCK_COMMENT_END = %r{\*/}
57
+ BLOCK_COMMENT_START = %r{/\*}
58
+ BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
59
+
60
+ # Template literal patterns
61
+ TEMPLATE_LITERAL_START = /\A`/
62
+ TEMPLATE_LITERAL_END = /`/
63
+
64
+ protected
65
+
66
+ def compiled_patterns
67
+ COMPILED_PATTERNS
68
+ end
69
+
70
+ # Handle multiline constructs
71
+ def handle_multiline_state(line, pos, state)
72
+ case state
73
+ when :block_comment
74
+ handle_block_comment(line, pos)
75
+ when :template_literal
76
+ handle_template_literal(line, pos)
77
+ else
78
+ [nil, nil, pos]
79
+ end
80
+ end
81
+
82
+ def check_multiline_start(line, pos)
83
+ rest = line[pos..]
84
+
85
+ # Check for template literal start
86
+ if rest.match?(TEMPLATE_LITERAL_START)
87
+ after_start = line[(pos + 1)..]
88
+ unless after_start&.include?("`")
89
+ text = line[pos..]
90
+ token = Token.new(
91
+ type: :string,
92
+ start_col: pos,
93
+ end_col: line.length - 1,
94
+ text:
95
+ )
96
+ return [:template_literal, token, line.length]
97
+ end
98
+ end
99
+
100
+ # Check for /* that doesn't have a matching */ on this line
101
+ start_match = rest.match(BLOCK_COMMENT_START)
102
+ return [nil, nil, pos] unless start_match
103
+
104
+ start_pos = pos + start_match.begin(0)
105
+ after_start = line[(start_pos + 2)..]
106
+
107
+ if after_start&.include?("*/")
108
+ [nil, nil, pos]
109
+ else
110
+ text = line[start_pos..]
111
+ token = Token.new(
112
+ type: :comment,
113
+ start_col: start_pos,
114
+ end_col: line.length - 1,
115
+ text:
116
+ )
117
+ [:block_comment, token, line.length]
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def handle_block_comment(line, pos)
124
+ end_match = line[pos..].match(BLOCK_COMMENT_END)
125
+ if end_match
126
+ end_pos = pos + end_match.begin(0) + 1
127
+ text = line[pos..end_pos]
128
+ token = Token.new(
129
+ type: :comment,
130
+ start_col: pos,
131
+ end_col: end_pos,
132
+ text:
133
+ )
134
+ [token, nil, end_pos + 1]
135
+ else
136
+ text = line[pos..]
137
+ token = if text.empty?
138
+ nil
139
+ else
140
+ Token.new(
141
+ type: :comment,
142
+ start_col: pos,
143
+ end_col: line.length - 1,
144
+ text:
145
+ )
146
+ end
147
+ [token, :block_comment, line.length]
148
+ end
149
+ end
150
+
151
+ def handle_template_literal(line, pos)
152
+ end_match = line[pos..].match(TEMPLATE_LITERAL_END)
153
+ if end_match
154
+ end_pos = pos + end_match.begin(0)
155
+ text = line[pos..end_pos]
156
+ token = Token.new(
157
+ type: :string,
158
+ start_col: pos,
159
+ end_col: end_pos,
160
+ text:
161
+ )
162
+ [token, nil, end_pos + 1]
163
+ else
164
+ text = line[pos..]
165
+ token = if text.empty?
166
+ nil
167
+ else
168
+ Token.new(
169
+ type: :string,
170
+ start_col: pos,
171
+ end_col: line.length - 1,
172
+ text:
173
+ )
174
+ end
175
+ [token, :template_literal, line.length]
176
+ end
177
+ end
178
+
179
+ def match_token(line, pos)
180
+ # Check for start of template literal
181
+ if line[pos..].match?(TEMPLATE_LITERAL_START)
182
+ rest = line[(pos + 1)..]
183
+ return nil unless rest&.include?("`")
184
+ end
185
+
186
+ # Check for start of multiline comment
187
+ if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
188
+ rest = line[(pos + 2)..]
189
+ return nil unless rest&.include?("*/")
190
+ end
191
+
192
+ super
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Syntax
5
+ module Lexers
6
+ # Lexer for Markdown source files
7
+ class MarkdownLexer < LexerBase
8
+ # Pre-compiled patterns with \G anchor for position-specific matching
9
+ # Markdown is line-oriented, so we check line-start patterns separately
10
+ COMPILED_PATTERNS = [
11
+ # Inline code (backtick)
12
+ [:string, /\G`[^`]+`/],
13
+ # Bold with asterisks
14
+ [:keyword, /\G\*\*[^*]+\*\*/],
15
+ # Bold with underscores
16
+ [:keyword, /\G__[^_]+__/],
17
+ # Italic with asterisks
18
+ [:comment, /\G\*[^*]+\*/],
19
+ # Italic with underscores
20
+ [:comment, /\G_[^_]+_/],
21
+ # Strikethrough
22
+ [:comment, /\G~~[^~]+~~/],
23
+ # Link [text](url)
24
+ [:constant, /\G\[[^\]]+\]\([^)]+\)/],
25
+ # Image ![alt](url)
26
+ [:constant, /\G!\[[^\]]*\]\([^)]+\)/],
27
+ # Reference link [text][ref]
28
+ [:constant, /\G\[[^\]]+\]\[[^\]]*\]/],
29
+ # Autolink <url> or <email>
30
+ [:constant, /\G<[a-zA-Z][a-zA-Z0-9+.-]*:[^>]+>/],
31
+ # HTML tags
32
+ [:preprocessor, %r{\G</?[a-zA-Z][a-zA-Z0-9]*[^>]*>}]
33
+ ].freeze
34
+
35
+ # Line-start patterns (checked at beginning of line)
36
+ HEADING_PATTERN = /\A(\#{1,6})\s+(.*)$/
37
+ BLOCKQUOTE_PATTERN = /\A>\s*/
38
+ UNORDERED_LIST_PATTERN = /\A\s*[-*+]\s+/
39
+ ORDERED_LIST_PATTERN = /\A\s*\d+\.\s+/
40
+ HORIZONTAL_RULE_PATTERN = /\A([-*_])\s*\1\s*\1[\s\1]*$/
41
+ CODE_FENCE_START = /\A```(\w*)/
42
+ CODE_FENCE_END = /\A```\s*$/
43
+ LINK_DEFINITION_PATTERN = /\A\s*\[[^\]]+\]:\s+\S+/
44
+
45
+ # Override tokenize to handle line-start patterns
46
+ def tokenize(line, state = nil)
47
+ tokens = []
48
+ pos = 0
49
+
50
+ # Handle code fence state
51
+ if state == :code_fence
52
+ if line.match?(CODE_FENCE_END)
53
+ token = Token.new(
54
+ type: :string,
55
+ start_col: 0,
56
+ end_col: line.length - 1,
57
+ text: line
58
+ )
59
+ return [[token], nil]
60
+ else
61
+ unless line.empty?
62
+ token = Token.new(
63
+ type: :string,
64
+ start_col: 0,
65
+ end_col: line.length - 1,
66
+ text: line
67
+ )
68
+ end
69
+ return [token ? [token] : [], :code_fence]
70
+ end
71
+ end
72
+
73
+ # Check for code fence start
74
+ fence_match = line.match(CODE_FENCE_START)
75
+ if fence_match
76
+ token = Token.new(
77
+ type: :string,
78
+ start_col: 0,
79
+ end_col: line.length - 1,
80
+ text: line
81
+ )
82
+ return [[token], :code_fence]
83
+ end
84
+
85
+ # Check line-start patterns
86
+ line_start_token = check_line_start(line)
87
+ if line_start_token
88
+ tokens << line_start_token
89
+ pos = line_start_token.end_col + 1
90
+ end
91
+
92
+ # Process rest of line with inline patterns
93
+ while pos < line.length
94
+ # Skip whitespace
95
+ if line[pos] =~ /\s/
96
+ pos += 1
97
+ next
98
+ end
99
+
100
+ # Try to match a token
101
+ token = match_token(line, pos)
102
+ if token
103
+ tokens << token
104
+ pos = token.end_col + 1
105
+ else
106
+ pos += 1
107
+ end
108
+ end
109
+
110
+ [tokens, nil]
111
+ end
112
+
113
+ protected
114
+
115
+ def compiled_patterns
116
+ COMPILED_PATTERNS
117
+ end
118
+
119
+ private
120
+
121
+ def check_line_start(line)
122
+ # Heading
123
+ heading_match = line.match(HEADING_PATTERN)
124
+ if heading_match
125
+ level = heading_match[1].length
126
+ return Token.new(
127
+ type: :keyword,
128
+ start_col: 0,
129
+ end_col: level - 1,
130
+ text: heading_match[1]
131
+ )
132
+ end
133
+
134
+ # Horizontal rule
135
+ if line.match?(HORIZONTAL_RULE_PATTERN)
136
+ return Token.new(
137
+ type: :comment,
138
+ start_col: 0,
139
+ end_col: line.length - 1,
140
+ text: line
141
+ )
142
+ end
143
+
144
+ # Link definition
145
+ if line.match?(LINK_DEFINITION_PATTERN)
146
+ return Token.new(
147
+ type: :constant,
148
+ start_col: 0,
149
+ end_col: line.length - 1,
150
+ text: line
151
+ )
152
+ end
153
+
154
+ # Blockquote
155
+ blockquote_match = line.match(BLOCKQUOTE_PATTERN)
156
+ if blockquote_match
157
+ return Token.new(
158
+ type: :comment,
159
+ start_col: 0,
160
+ end_col: blockquote_match[0].length - 1,
161
+ text: blockquote_match[0]
162
+ )
163
+ end
164
+
165
+ # Unordered list
166
+ list_match = line.match(UNORDERED_LIST_PATTERN)
167
+ if list_match
168
+ return Token.new(
169
+ type: :operator,
170
+ start_col: 0,
171
+ end_col: list_match[0].length - 1,
172
+ text: list_match[0]
173
+ )
174
+ end
175
+
176
+ # Ordered list
177
+ ordered_match = line.match(ORDERED_LIST_PATTERN)
178
+ if ordered_match
179
+ return Token.new(
180
+ type: :number,
181
+ start_col: 0,
182
+ end_col: ordered_match[0].length - 1,
183
+ text: ordered_match[0]
184
+ )
185
+ end
186
+
187
+ nil
188
+ end
189
+
190
+ def match_token(line, pos)
191
+ rest = line[pos..]
192
+
193
+ COMPILED_PATTERNS.each do |type, pattern|
194
+ match = rest.match(pattern)
195
+ next unless match&.begin(0)&.zero?
196
+
197
+ return Token.new(
198
+ type:,
199
+ start_col: pos,
200
+ end_col: pos + match[0].length - 1,
201
+ text: match[0]
202
+ )
203
+ end
204
+
205
+ nil
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Syntax
5
+ module Lexers
6
+ # Lexer for Ruby source code
7
+ class RubyLexer < LexerBase
8
+ KEYWORDS = %w[
9
+ BEGIN END __ENCODING__ __END__ __FILE__ __LINE__
10
+ alias and begin break case class def defined? do
11
+ else elsif end ensure false for if in module next
12
+ nil not or redo rescue retry return self super then
13
+ true undef unless until when while yield
14
+ require require_relative attr_reader attr_writer attr_accessor
15
+ private protected public include extend prepend
16
+ raise fail catch throw
17
+ ].freeze
18
+
19
+ # Pre-compiled patterns with \G anchor for position-specific matching
20
+ # These are compiled once at class load time
21
+ COMPILED_PATTERNS = [
22
+ # Single line comment
23
+ [:comment, /\G#.*/],
24
+ # Double quoted string (with escape handling)
25
+ [:string, /\G"(?:[^"\\]|\\.)*"/],
26
+ # Single quoted string (with escape handling)
27
+ [:string, /\G'(?:[^'\\]|\\.)*'/],
28
+ # Symbols
29
+ [:symbol, /\G:[a-zA-Z_][a-zA-Z0-9_]*[?!]?/],
30
+ # Float numbers (must be before integer)
31
+ [:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
32
+ # Hexadecimal
33
+ [:number, /\G\b0x[0-9a-f]+\b/i],
34
+ # Octal
35
+ [:number, /\G\b0o?[0-7]+\b/],
36
+ # Binary
37
+ [:number, /\G\b0b[01]+\b/i],
38
+ # Integer
39
+ [:number, /\G\b\d+\b/],
40
+ # Constants (capitalized identifiers)
41
+ [:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
42
+ # Keywords
43
+ [:keyword, /\G\b(?:BEGIN|END|__ENCODING__|__END__|__FILE__|__LINE__|alias|and|begin|break|case|class|def|defined\?|do|else|elsif|end|ensure|false|for|if|in|module|next|nil|not|or|redo|rescue|retry|return|self|super|then|true|undef|unless|until|when|while|yield|require|require_relative|attr_reader|attr_writer|attr_accessor|private|protected|public|include|extend|prepend|raise|fail|catch|throw)\b/],
44
+ # Instance variables (@foo, @@foo)
45
+ [:instance_variable, /\G@{1,2}[a-zA-Z_][a-zA-Z0-9_]*/],
46
+ # Global variables
47
+ [:global_variable, /\G\$[a-zA-Z_][a-zA-Z0-9_]*/],
48
+ # Method calls (.to_i, .each, .map!, .empty?, etc.)
49
+ [:method_call, /\G\.[a-z_][a-zA-Z0-9_]*[?!]?/],
50
+ # Identifiers (including method names with ? or !)
51
+ [:identifier, /\G\b[a-z_][a-zA-Z0-9_]*[?!]?/],
52
+ # Operators
53
+ [:operator, %r{\G(?:[+\-*/%&|^~<>=!]+|<<|>>|\*\*)}]
54
+ ].freeze
55
+
56
+ # Multiline patterns (also pre-compiled)
57
+ BLOCK_COMMENT_END = /\A=end\b/
58
+ BLOCK_COMMENT_START = /\A=begin\b/
59
+
60
+ protected
61
+
62
+ # Use pre-compiled class-level patterns
63
+ def compiled_patterns
64
+ COMPILED_PATTERNS
65
+ end
66
+
67
+ # Handle =begin...=end block comments
68
+ def handle_multiline_state(line, pos, state)
69
+ return [nil, nil, pos] unless state == :block_comment
70
+
71
+ # Check for =end
72
+ text = line[pos..]
73
+ if BLOCK_COMMENT_END.match?(text)
74
+ token = Token.new(
75
+ type: :comment,
76
+ start_col: pos,
77
+ end_col: line.length - 1,
78
+ text:
79
+ )
80
+ [token, nil, line.length]
81
+ else
82
+ # Entire line is part of block comment
83
+ unless text.empty?
84
+ token = Token.new(
85
+ type: :comment,
86
+ start_col: pos,
87
+ end_col: line.length - 1,
88
+ text:
89
+ )
90
+ end
91
+ [token, :block_comment, line.length]
92
+ end
93
+ end
94
+
95
+ # Check for =begin block comment start
96
+ def check_multiline_start(line, pos)
97
+ return [nil, nil, pos] unless pos.zero?
98
+
99
+ if BLOCK_COMMENT_START.match?(line)
100
+ token = Token.new(
101
+ type: :comment,
102
+ start_col: 0,
103
+ end_col: line.length - 1,
104
+ text: line
105
+ )
106
+ [:block_comment, token, line.length]
107
+ else
108
+ [nil, nil, pos]
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end