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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +163 -0
- data/CHANGELOG.md +448 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -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 +149 -0
- data/exe/mui +1 -2
- data/lib/mui/autocmd.rb +66 -0
- data/lib/mui/buffer.rb +275 -0
- data/lib/mui/buffer_word_cache.rb +131 -0
- data/lib/mui/buffer_word_completer.rb +77 -0
- data/lib/mui/color_manager.rb +136 -0
- data/lib/mui/color_scheme.rb +63 -0
- data/lib/mui/command_completer.rb +30 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +167 -0
- data/lib/mui/command_registry.rb +44 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +58 -0
- data/lib/mui/editor.rb +395 -0
- data/lib/mui/error.rb +29 -0
- data/lib/mui/file_completer.rb +51 -0
- data/lib/mui/floating_window.rb +161 -0
- data/lib/mui/handler_result.rb +107 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +27 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
- data/lib/mui/input.rb +17 -0
- data/lib/mui/insert_completion_renderer.rb +92 -0
- data/lib/mui/insert_completion_state.rb +77 -0
- data/lib/mui/job.rb +81 -0
- data/lib/mui/job_manager.rb +113 -0
- data/lib/mui/key_code.rb +30 -0
- data/lib/mui/key_handler/base.rb +187 -0
- data/lib/mui/key_handler/command_mode.rb +511 -0
- data/lib/mui/key_handler/insert_mode.rb +323 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +552 -0
- data/lib/mui/key_handler/operators/base_operator.rb +134 -0
- data/lib/mui/key_handler/operators/change_operator.rb +179 -0
- data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
- data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +191 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +402 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -0
- data/lib/mui/key_notation_parser.rb +152 -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/layout/calculator.rb +15 -0
- data/lib/mui/layout/leaf_node.rb +33 -0
- data/lib/mui/layout/node.rb +29 -0
- data/lib/mui/layout/split_node.rb +132 -0
- data/lib/mui/line_renderer.rb +173 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +186 -0
- data/lib/mui/motion.rb +139 -0
- data/lib/mui/plugin.rb +35 -0
- data/lib/mui/plugin_manager.rb +106 -0
- data/lib/mui/register.rb +110 -0
- data/lib/mui/screen.rb +103 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +121 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +106 -0
- data/lib/mui/syntax/lexer_base.rb +106 -0
- data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/syntax/token.rb +42 -0
- data/lib/mui/syntax/token_cache.rb +91 -0
- data/lib/mui/tab_bar_renderer.rb +87 -0
- data/lib/mui/tab_manager.rb +96 -0
- data/lib/mui/tab_page.rb +35 -0
- data/lib/mui/terminal_adapter/base.rb +92 -0
- data/lib/mui/terminal_adapter/curses.rb +164 -0
- data/lib/mui/terminal_adapter.rb +4 -0
- data/lib/mui/themes/default.rb +315 -0
- data/lib/mui/undo_manager.rb +83 -0
- data/lib/mui/undoable_action.rb +175 -0
- data/lib/mui/unicode_width.rb +100 -0
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +201 -0
- data/lib/mui/window_manager.rb +256 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +171 -2
- 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 
|
|
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
|