mui 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/.rubocop_todo.yml +18 -10
- data/CHANGELOG.md +162 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +314 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +155 -0
- data/lib/mui/color_manager.rb +140 -6
- data/lib/mui/color_scheme.rb +1 -0
- data/lib/mui/command_completer.rb +11 -2
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +32 -2
- data/lib/mui/command_registry.rb +21 -2
- data/lib/mui/config.rb +3 -1
- data/lib/mui/editor.rb +90 -2
- data/lib/mui/floating_window.rb +53 -1
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +4 -1
- data/lib/mui/insert_completion_state.rb +15 -2
- data/lib/mui/key_handler/base.rb +87 -0
- data/lib/mui/key_handler/command_mode.rb +68 -0
- data/lib/mui/key_handler/insert_mode.rb +10 -41
- data/lib/mui/key_handler/normal_mode.rb +24 -51
- data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
- data/lib/mui/key_handler/search_mode.rb +10 -7
- data/lib/mui/key_handler/visual_mode.rb +15 -10
- data/lib/mui/key_notation_parser.rb +159 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/line_renderer.rb +52 -1
- data/lib/mui/mode_manager.rb +3 -2
- data/lib/mui/screen.rb +30 -6
- data/lib/mui/search_state.rb +74 -27
- data/lib/mui/syntax/language_detector.rb +33 -1
- data/lib/mui/syntax/lexers/c_lexer.rb +2 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +207 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +219 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +3 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +150 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +225 -0
- data/lib/mui/terminal_adapter/base.rb +21 -0
- data/lib/mui/terminal_adapter/curses.rb +37 -11
- data/lib/mui/themes/default.rb +263 -132
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +105 -39
- data/lib/mui/window_manager.rb +7 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +15 -0
- metadata +26 -3
|
@@ -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,219 @@
|
|
|
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
|
+
# Override tokenize to post-process function definitions
|
|
65
|
+
def tokenize(line, state = nil)
|
|
66
|
+
tokens, new_state = super
|
|
67
|
+
|
|
68
|
+
# Convert identifiers after 'function' keyword to function_definition
|
|
69
|
+
tokens.each_with_index do |token, i|
|
|
70
|
+
next unless i.positive? &&
|
|
71
|
+
tokens[i - 1].type == :keyword &&
|
|
72
|
+
tokens[i - 1].text == "function" &&
|
|
73
|
+
token.type == :identifier
|
|
74
|
+
|
|
75
|
+
tokens[i] = Token.new(
|
|
76
|
+
type: :function_definition,
|
|
77
|
+
start_col: token.start_col,
|
|
78
|
+
end_col: token.end_col,
|
|
79
|
+
text: token.text
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[tokens, new_state]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
protected
|
|
87
|
+
|
|
88
|
+
def compiled_patterns
|
|
89
|
+
COMPILED_PATTERNS
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handle multiline constructs
|
|
93
|
+
def handle_multiline_state(line, pos, state)
|
|
94
|
+
case state
|
|
95
|
+
when :block_comment
|
|
96
|
+
handle_block_comment(line, pos)
|
|
97
|
+
when :template_literal
|
|
98
|
+
handle_template_literal(line, pos)
|
|
99
|
+
else
|
|
100
|
+
[nil, nil, pos]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def check_multiline_start(line, pos)
|
|
105
|
+
rest = line[pos..]
|
|
106
|
+
|
|
107
|
+
# Check for template literal start
|
|
108
|
+
if rest.match?(TEMPLATE_LITERAL_START)
|
|
109
|
+
after_start = line[(pos + 1)..]
|
|
110
|
+
unless after_start&.include?("`")
|
|
111
|
+
text = line[pos..]
|
|
112
|
+
token = Token.new(
|
|
113
|
+
type: :string,
|
|
114
|
+
start_col: pos,
|
|
115
|
+
end_col: line.length - 1,
|
|
116
|
+
text:
|
|
117
|
+
)
|
|
118
|
+
return [:template_literal, token, line.length]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check for /* that doesn't have a matching */ on this line
|
|
123
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
124
|
+
return [nil, nil, pos] unless start_match
|
|
125
|
+
|
|
126
|
+
start_pos = pos + start_match.begin(0)
|
|
127
|
+
after_start = line[(start_pos + 2)..]
|
|
128
|
+
|
|
129
|
+
if after_start&.include?("*/")
|
|
130
|
+
[nil, nil, pos]
|
|
131
|
+
else
|
|
132
|
+
text = line[start_pos..]
|
|
133
|
+
token = Token.new(
|
|
134
|
+
type: :comment,
|
|
135
|
+
start_col: start_pos,
|
|
136
|
+
end_col: line.length - 1,
|
|
137
|
+
text:
|
|
138
|
+
)
|
|
139
|
+
[:block_comment, token, line.length]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def handle_block_comment(line, pos)
|
|
146
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
147
|
+
if end_match
|
|
148
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
149
|
+
text = line[pos..end_pos]
|
|
150
|
+
token = Token.new(
|
|
151
|
+
type: :comment,
|
|
152
|
+
start_col: pos,
|
|
153
|
+
end_col: end_pos,
|
|
154
|
+
text:
|
|
155
|
+
)
|
|
156
|
+
[token, nil, end_pos + 1]
|
|
157
|
+
else
|
|
158
|
+
text = line[pos..]
|
|
159
|
+
token = if text.empty?
|
|
160
|
+
nil
|
|
161
|
+
else
|
|
162
|
+
Token.new(
|
|
163
|
+
type: :comment,
|
|
164
|
+
start_col: pos,
|
|
165
|
+
end_col: line.length - 1,
|
|
166
|
+
text:
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
[token, :block_comment, line.length]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def handle_template_literal(line, pos)
|
|
174
|
+
end_match = line[pos..].match(TEMPLATE_LITERAL_END)
|
|
175
|
+
if end_match
|
|
176
|
+
end_pos = pos + end_match.begin(0)
|
|
177
|
+
text = line[pos..end_pos]
|
|
178
|
+
token = Token.new(
|
|
179
|
+
type: :string,
|
|
180
|
+
start_col: pos,
|
|
181
|
+
end_col: end_pos,
|
|
182
|
+
text:
|
|
183
|
+
)
|
|
184
|
+
[token, nil, end_pos + 1]
|
|
185
|
+
else
|
|
186
|
+
text = line[pos..]
|
|
187
|
+
token = if text.empty?
|
|
188
|
+
nil
|
|
189
|
+
else
|
|
190
|
+
Token.new(
|
|
191
|
+
type: :string,
|
|
192
|
+
start_col: pos,
|
|
193
|
+
end_col: line.length - 1,
|
|
194
|
+
text:
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
[token, :template_literal, line.length]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def match_token(line, pos)
|
|
202
|
+
# Check for start of template literal
|
|
203
|
+
if line[pos..].match?(TEMPLATE_LITERAL_START)
|
|
204
|
+
rest = line[(pos + 1)..]
|
|
205
|
+
return nil unless rest&.include?("`")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Check for start of multiline comment
|
|
209
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
210
|
+
rest = line[(pos + 2)..]
|
|
211
|
+
return nil unless rest&.include?("*/")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
super
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
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 
|
|
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
|
|
@@ -47,6 +47,9 @@ module Mui
|
|
|
47
47
|
[:global_variable, /\G\$[a-zA-Z_][a-zA-Z0-9_]*/],
|
|
48
48
|
# Method calls (.to_i, .each, .map!, .empty?, etc.)
|
|
49
49
|
[:method_call, /\G\.[a-z_][a-zA-Z0-9_]*[?!]?/],
|
|
50
|
+
# Function/method definition names (def の後)
|
|
51
|
+
# Note: def self.method_name is handled by method_call pattern (same color)
|
|
52
|
+
[:function_definition, /\G(?<=def )[a-z_][a-zA-Z0-9_]*[?!=]?/],
|
|
50
53
|
# Identifiers (including method names with ? or !)
|
|
51
54
|
[:identifier, /\G\b[a-z_][a-zA-Z0-9_]*[?!]?/],
|
|
52
55
|
# Operators
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for Rust source code
|
|
7
|
+
class RustLexer < LexerBase
|
|
8
|
+
# Rust keywords
|
|
9
|
+
KEYWORDS = %w[
|
|
10
|
+
as async await break const continue crate dyn else enum extern
|
|
11
|
+
false fn for if impl in let loop match mod move mut pub ref
|
|
12
|
+
return self Self static struct super trait true type unsafe use
|
|
13
|
+
where while
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# Rust primitive types
|
|
17
|
+
TYPES = %w[
|
|
18
|
+
bool char str
|
|
19
|
+
i8 i16 i32 i64 i128 isize
|
|
20
|
+
u8 u16 u32 u64 u128 usize
|
|
21
|
+
f32 f64
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
25
|
+
COMPILED_PATTERNS = [
|
|
26
|
+
# Doc comments (must be before regular comments)
|
|
27
|
+
[:comment, %r{\G///.*}],
|
|
28
|
+
[:comment, %r{\G//!.*}],
|
|
29
|
+
# Single line comment
|
|
30
|
+
[:comment, %r{\G//.*}],
|
|
31
|
+
# Single-line block comment /* ... */ on one line
|
|
32
|
+
[:comment, %r{\G/\*.*?\*/}],
|
|
33
|
+
# Attributes
|
|
34
|
+
[:preprocessor, /\G#!\[[^\]]*\]/],
|
|
35
|
+
[:preprocessor, /\G#\[[^\]]*\]/],
|
|
36
|
+
# Character literal - single char or escape sequence (must be before lifetime)
|
|
37
|
+
[:char, /\G'(?:[^'\\]|\\.)'/],
|
|
38
|
+
# Lifetime
|
|
39
|
+
[:symbol, /\G'[a-z_][a-zA-Z0-9_]*/],
|
|
40
|
+
# Raw string r#"..."#
|
|
41
|
+
[:string, /\Gr#+"[^"]*"#+/],
|
|
42
|
+
# Byte string
|
|
43
|
+
[:string, /\Gb"(?:[^"\\]|\\.)*"/],
|
|
44
|
+
# Regular string
|
|
45
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
46
|
+
# Float numbers (must be before integer)
|
|
47
|
+
[:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?(?:f32|f64)?\b/i],
|
|
48
|
+
# Hexadecimal
|
|
49
|
+
[:number, /\G\b0x[0-9a-fA-F_]+(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize)?\b/],
|
|
50
|
+
# Octal
|
|
51
|
+
[:number, /\G\b0o[0-7_]+(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize)?\b/],
|
|
52
|
+
# Binary
|
|
53
|
+
[:number, /\G\b0b[01_]+(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize)?\b/],
|
|
54
|
+
# Integer
|
|
55
|
+
[:number, /\G\b\d[0-9_]*(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize)?\b/],
|
|
56
|
+
# Macros (identifier followed by !)
|
|
57
|
+
[:macro, /\G\b[a-z_][a-zA-Z0-9_]*!/],
|
|
58
|
+
# Primitive types
|
|
59
|
+
[:type, /\G\b(?:bool|char|str|i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64)\b/],
|
|
60
|
+
# Keywords
|
|
61
|
+
[:keyword, /\G\b(?:as|async|await|break|const|continue|crate|dyn|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|true|type|unsafe|use|where|while)\b/],
|
|
62
|
+
# Function definition names (fn の後)
|
|
63
|
+
[:function_definition, /\G(?<=fn )[a-z_][a-zA-Z0-9_]*/],
|
|
64
|
+
# Type names (start with uppercase)
|
|
65
|
+
[:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
|
|
66
|
+
# Regular identifiers
|
|
67
|
+
[:identifier, /\G\b[a-z_][a-zA-Z0-9_]*\b/],
|
|
68
|
+
# Operators
|
|
69
|
+
[:operator, %r{\G(?:&&|\|\||<<|>>|=>|->|::|\.\.=?|[+\-*/%&|^<>=!]=?|\?)}]
|
|
70
|
+
].freeze
|
|
71
|
+
|
|
72
|
+
# Multiline comment patterns (pre-compiled)
|
|
73
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
74
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
75
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
def compiled_patterns
|
|
80
|
+
COMPILED_PATTERNS
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Handle /* ... */ block comments that span multiple lines
|
|
84
|
+
def handle_multiline_state(line, pos, state)
|
|
85
|
+
return [nil, nil, pos] unless state == :block_comment
|
|
86
|
+
|
|
87
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
88
|
+
if end_match
|
|
89
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
90
|
+
text = line[pos..end_pos]
|
|
91
|
+
token = Token.new(
|
|
92
|
+
type: :comment,
|
|
93
|
+
start_col: pos,
|
|
94
|
+
end_col: end_pos,
|
|
95
|
+
text:
|
|
96
|
+
)
|
|
97
|
+
[token, nil, end_pos + 1]
|
|
98
|
+
else
|
|
99
|
+
text = line[pos..]
|
|
100
|
+
token = if text.empty?
|
|
101
|
+
nil
|
|
102
|
+
else
|
|
103
|
+
Token.new(
|
|
104
|
+
type: :comment,
|
|
105
|
+
start_col: pos,
|
|
106
|
+
end_col: line.length - 1,
|
|
107
|
+
text:
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
[token, :block_comment, line.length]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def check_multiline_start(line, pos)
|
|
115
|
+
rest = line[pos..]
|
|
116
|
+
|
|
117
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
118
|
+
return [nil, nil, pos] unless start_match
|
|
119
|
+
|
|
120
|
+
start_pos = pos + start_match.begin(0)
|
|
121
|
+
after_start = line[(start_pos + 2)..]
|
|
122
|
+
|
|
123
|
+
if after_start&.include?("*/")
|
|
124
|
+
[nil, nil, pos]
|
|
125
|
+
else
|
|
126
|
+
text = line[start_pos..]
|
|
127
|
+
token = Token.new(
|
|
128
|
+
type: :comment,
|
|
129
|
+
start_col: start_pos,
|
|
130
|
+
end_col: line.length - 1,
|
|
131
|
+
text:
|
|
132
|
+
)
|
|
133
|
+
[:block_comment, token, line.length]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def match_token(line, pos)
|
|
140
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
141
|
+
rest = line[(pos + 2)..]
|
|
142
|
+
return nil unless rest&.include?("*/")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|