mui 0.2.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 +13 -8
- data/CHANGELOG.md +99 -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/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 +78 -2
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
- 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 +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/line_renderer.rb +52 -1
- data/lib/mui/mode_manager.rb +3 -2
- data/lib/mui/screen.rb +24 -6
- data/lib/mui/search_state.rb +61 -28
- data/lib/mui/syntax/language_detector.rb +33 -1
- 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/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/terminal_adapter/curses.rb +13 -11
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +83 -40
- 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,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,148 @@
|
|
|
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
|
+
# Type names (start with uppercase)
|
|
63
|
+
[:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
|
|
64
|
+
# Regular identifiers
|
|
65
|
+
[:identifier, /\G\b[a-z_][a-zA-Z0-9_]*\b/],
|
|
66
|
+
# Operators
|
|
67
|
+
[:operator, %r{\G(?:&&|\|\||<<|>>|=>|->|::|\.\.=?|[+\-*/%&|^<>=!]=?|\?)}]
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
# Multiline comment patterns (pre-compiled)
|
|
71
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
72
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
73
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
def compiled_patterns
|
|
78
|
+
COMPILED_PATTERNS
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Handle /* ... */ block comments that span multiple lines
|
|
82
|
+
def handle_multiline_state(line, pos, state)
|
|
83
|
+
return [nil, nil, pos] unless state == :block_comment
|
|
84
|
+
|
|
85
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
86
|
+
if end_match
|
|
87
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
88
|
+
text = line[pos..end_pos]
|
|
89
|
+
token = Token.new(
|
|
90
|
+
type: :comment,
|
|
91
|
+
start_col: pos,
|
|
92
|
+
end_col: end_pos,
|
|
93
|
+
text:
|
|
94
|
+
)
|
|
95
|
+
[token, nil, end_pos + 1]
|
|
96
|
+
else
|
|
97
|
+
text = line[pos..]
|
|
98
|
+
token = if text.empty?
|
|
99
|
+
nil
|
|
100
|
+
else
|
|
101
|
+
Token.new(
|
|
102
|
+
type: :comment,
|
|
103
|
+
start_col: pos,
|
|
104
|
+
end_col: line.length - 1,
|
|
105
|
+
text:
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
[token, :block_comment, line.length]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def check_multiline_start(line, pos)
|
|
113
|
+
rest = line[pos..]
|
|
114
|
+
|
|
115
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
116
|
+
return [nil, nil, pos] unless start_match
|
|
117
|
+
|
|
118
|
+
start_pos = pos + start_match.begin(0)
|
|
119
|
+
after_start = line[(start_pos + 2)..]
|
|
120
|
+
|
|
121
|
+
if after_start&.include?("*/")
|
|
122
|
+
[nil, nil, pos]
|
|
123
|
+
else
|
|
124
|
+
text = line[start_pos..]
|
|
125
|
+
token = Token.new(
|
|
126
|
+
type: :comment,
|
|
127
|
+
start_col: start_pos,
|
|
128
|
+
end_col: line.length - 1,
|
|
129
|
+
text:
|
|
130
|
+
)
|
|
131
|
+
[:block_comment, token, line.length]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def match_token(line, pos)
|
|
138
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
139
|
+
rest = line[(pos + 2)..]
|
|
140
|
+
return nil unless rest&.include?("*/")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for TypeScript source code
|
|
7
|
+
# Extends JavaScript with additional TypeScript-specific patterns
|
|
8
|
+
class TypeScriptLexer < LexerBase
|
|
9
|
+
# TypeScript keywords (JavaScript keywords + TypeScript-specific)
|
|
10
|
+
KEYWORDS = %w[
|
|
11
|
+
async await break case catch class const continue debugger default
|
|
12
|
+
delete do else export extends finally for function if import in
|
|
13
|
+
instanceof let new return static switch throw try typeof var void
|
|
14
|
+
while with yield
|
|
15
|
+
abstract as asserts declare enum implements interface keyof
|
|
16
|
+
module namespace never override private protected public readonly
|
|
17
|
+
type unknown
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# TypeScript constants
|
|
21
|
+
CONSTANTS = %w[true false null undefined NaN Infinity this super].freeze
|
|
22
|
+
|
|
23
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
24
|
+
COMPILED_PATTERNS = [
|
|
25
|
+
# Single line comment
|
|
26
|
+
[:comment, %r{\G//.*}],
|
|
27
|
+
# Single-line block comment /* ... */ on one line
|
|
28
|
+
[:comment, %r{\G/\*.*?\*/}],
|
|
29
|
+
# Template literal (single line)
|
|
30
|
+
[:string, /\G`[^`]*`/],
|
|
31
|
+
# Double quoted string (with escape handling)
|
|
32
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
33
|
+
# Single quoted string (with escape handling)
|
|
34
|
+
[:string, /\G'(?:[^'\\]|\\.)*'/],
|
|
35
|
+
# Regular expression literal
|
|
36
|
+
[:regex, %r{\G/(?:[^/\\]|\\.)+/[gimsuy]*}],
|
|
37
|
+
# Float numbers (must be before integer)
|
|
38
|
+
[:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
|
|
39
|
+
# Hexadecimal
|
|
40
|
+
[:number, /\G\b0x[0-9a-fA-F]+n?\b/i],
|
|
41
|
+
# Octal
|
|
42
|
+
[:number, /\G\b0o[0-7]+n?\b/i],
|
|
43
|
+
# Binary
|
|
44
|
+
[:number, /\G\b0b[01]+n?\b/i],
|
|
45
|
+
# Integer (with optional BigInt suffix)
|
|
46
|
+
[:number, /\G\b\d+n?\b/],
|
|
47
|
+
# Constants (true, false, null, undefined, NaN, Infinity, this, super)
|
|
48
|
+
[:constant, /\G\b(?:true|false|null|undefined|NaN|Infinity|this|super)\b/],
|
|
49
|
+
# TypeScript + JavaScript keywords
|
|
50
|
+
[:keyword, /\G\b(?:abstract|as|asserts|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|function|if|implements|import|in|instanceof|interface|keyof|let|module|namespace|never|new|override|private|protected|public|readonly|return|static|switch|throw|try|type|typeof|unknown|var|void|while|with|yield)\b/],
|
|
51
|
+
# Generic type parameters <T> and type annotations
|
|
52
|
+
[:type, /\G<[A-Z][a-zA-Z0-9_,\s]*>/],
|
|
53
|
+
# Class/constructor/type names (start with uppercase)
|
|
54
|
+
[:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
|
|
55
|
+
# Regular identifiers
|
|
56
|
+
[:identifier, /\G\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/],
|
|
57
|
+
# Operators (=> must come before = patterns)
|
|
58
|
+
[:operator, %r{\G(?:\.{3}|=>|&&=?|\|\|=?|\?\?=?|===?|!==?|>>>?=?|<<=?|\+\+|--|\?\.?|[+\-*/%&|^<>=!]=?)}]
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
# Multiline patterns (pre-compiled)
|
|
62
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
63
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
64
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
65
|
+
|
|
66
|
+
# Template literal patterns
|
|
67
|
+
TEMPLATE_LITERAL_START = /\A`/
|
|
68
|
+
TEMPLATE_LITERAL_END = /`/
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
|
|
72
|
+
def compiled_patterns
|
|
73
|
+
COMPILED_PATTERNS
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Handle multiline constructs
|
|
77
|
+
def handle_multiline_state(line, pos, state)
|
|
78
|
+
case state
|
|
79
|
+
when :block_comment
|
|
80
|
+
handle_block_comment(line, pos)
|
|
81
|
+
when :template_literal
|
|
82
|
+
handle_template_literal(line, pos)
|
|
83
|
+
else
|
|
84
|
+
[nil, nil, pos]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def check_multiline_start(line, pos)
|
|
89
|
+
rest = line[pos..]
|
|
90
|
+
|
|
91
|
+
# Check for template literal start
|
|
92
|
+
if rest.match?(TEMPLATE_LITERAL_START)
|
|
93
|
+
after_start = line[(pos + 1)..]
|
|
94
|
+
unless after_start&.include?("`")
|
|
95
|
+
text = line[pos..]
|
|
96
|
+
token = Token.new(
|
|
97
|
+
type: :string,
|
|
98
|
+
start_col: pos,
|
|
99
|
+
end_col: line.length - 1,
|
|
100
|
+
text:
|
|
101
|
+
)
|
|
102
|
+
return [:template_literal, token, line.length]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check for /* that doesn't have a matching */ on this line
|
|
107
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
108
|
+
return [nil, nil, pos] unless start_match
|
|
109
|
+
|
|
110
|
+
start_pos = pos + start_match.begin(0)
|
|
111
|
+
after_start = line[(start_pos + 2)..]
|
|
112
|
+
|
|
113
|
+
if after_start&.include?("*/")
|
|
114
|
+
[nil, nil, pos]
|
|
115
|
+
else
|
|
116
|
+
text = line[start_pos..]
|
|
117
|
+
token = Token.new(
|
|
118
|
+
type: :comment,
|
|
119
|
+
start_col: start_pos,
|
|
120
|
+
end_col: line.length - 1,
|
|
121
|
+
text:
|
|
122
|
+
)
|
|
123
|
+
[:block_comment, token, line.length]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def handle_block_comment(line, pos)
|
|
130
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
131
|
+
if end_match
|
|
132
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
133
|
+
text = line[pos..end_pos]
|
|
134
|
+
token = Token.new(
|
|
135
|
+
type: :comment,
|
|
136
|
+
start_col: pos,
|
|
137
|
+
end_col: end_pos,
|
|
138
|
+
text:
|
|
139
|
+
)
|
|
140
|
+
[token, nil, end_pos + 1]
|
|
141
|
+
else
|
|
142
|
+
text = line[pos..]
|
|
143
|
+
token = if text.empty?
|
|
144
|
+
nil
|
|
145
|
+
else
|
|
146
|
+
Token.new(
|
|
147
|
+
type: :comment,
|
|
148
|
+
start_col: pos,
|
|
149
|
+
end_col: line.length - 1,
|
|
150
|
+
text:
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
[token, :block_comment, line.length]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def handle_template_literal(line, pos)
|
|
158
|
+
end_match = line[pos..].match(TEMPLATE_LITERAL_END)
|
|
159
|
+
if end_match
|
|
160
|
+
end_pos = pos + end_match.begin(0)
|
|
161
|
+
text = line[pos..end_pos]
|
|
162
|
+
token = Token.new(
|
|
163
|
+
type: :string,
|
|
164
|
+
start_col: pos,
|
|
165
|
+
end_col: end_pos,
|
|
166
|
+
text:
|
|
167
|
+
)
|
|
168
|
+
[token, nil, end_pos + 1]
|
|
169
|
+
else
|
|
170
|
+
text = line[pos..]
|
|
171
|
+
token = if text.empty?
|
|
172
|
+
nil
|
|
173
|
+
else
|
|
174
|
+
Token.new(
|
|
175
|
+
type: :string,
|
|
176
|
+
start_col: pos,
|
|
177
|
+
end_col: line.length - 1,
|
|
178
|
+
text:
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
[token, :template_literal, line.length]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def match_token(line, pos)
|
|
186
|
+
# Check for start of template literal
|
|
187
|
+
if line[pos..].match?(TEMPLATE_LITERAL_START)
|
|
188
|
+
rest = line[(pos + 1)..]
|
|
189
|
+
return nil unless rest&.include?("`")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check for start of multiline comment
|
|
193
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
194
|
+
rest = line[(pos + 2)..]
|
|
195
|
+
return nil unless rest&.include?("*/")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
super
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -8,6 +8,7 @@ module Mui
|
|
|
8
8
|
class Curses < Base
|
|
9
9
|
def init
|
|
10
10
|
::Curses.init_screen
|
|
11
|
+
::Curses.ESCDELAY = 10
|
|
11
12
|
::Curses.raw
|
|
12
13
|
::Curses.noecho
|
|
13
14
|
::Curses.curs_set(1)
|
|
@@ -133,18 +134,19 @@ module Mui
|
|
|
133
134
|
public
|
|
134
135
|
|
|
135
136
|
def getch_nonblock
|
|
136
|
-
::Curses.stdscr.
|
|
137
|
+
::Curses.stdscr.timeout = 0
|
|
138
|
+
|
|
137
139
|
key = ::Curses.getch
|
|
138
|
-
|
|
139
|
-
return key
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
|
|
141
|
+
return if key == -1 || key.nil?
|
|
142
|
+
return key unless key.is_a?(Integer)
|
|
143
|
+
return key if key.negative? || key > 255
|
|
144
|
+
|
|
145
|
+
if key >= 0x80
|
|
146
|
+
read_utf8_char(key)
|
|
147
|
+
else
|
|
148
|
+
key
|
|
149
|
+
end
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
def suspend
|
data/lib/mui/version.rb
CHANGED