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,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
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
# Represents a token identified by a lexer
|
|
6
|
+
# Tokens are immutable value objects
|
|
7
|
+
class Token
|
|
8
|
+
attr_reader :type, :start_col, :end_col, :text
|
|
9
|
+
|
|
10
|
+
# Token types:
|
|
11
|
+
# :keyword - language keywords (def, class, if, etc.)
|
|
12
|
+
# :string - string literals ("...", '...')
|
|
13
|
+
# :comment - comments (#..., //..., /* ... */)
|
|
14
|
+
# :number - numeric literals (123, 1.5, 0xFF)
|
|
15
|
+
# :symbol - Ruby symbols (:symbol)
|
|
16
|
+
# :constant - constants (ClassName, CONST)
|
|
17
|
+
# :operator - operators (+, -, =, etc.)
|
|
18
|
+
# :identifier - variable names, method names
|
|
19
|
+
# :preprocessor - C preprocessor directives (#include, #define)
|
|
20
|
+
# :char - character literals ('a')
|
|
21
|
+
def initialize(type:, start_col:, end_col:, text:)
|
|
22
|
+
@type = type
|
|
23
|
+
@start_col = start_col
|
|
24
|
+
@end_col = end_col
|
|
25
|
+
@text = text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ==(other)
|
|
29
|
+
return false unless other.is_a?(Token)
|
|
30
|
+
|
|
31
|
+
type == other.type &&
|
|
32
|
+
start_col == other.start_col &&
|
|
33
|
+
end_col == other.end_col &&
|
|
34
|
+
text == other.text
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def length
|
|
38
|
+
text.length
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
# Caches tokenization results on a per-line basis
|
|
6
|
+
# Handles multiline state propagation across lines
|
|
7
|
+
class TokenCache
|
|
8
|
+
# Number of lines to prefetch ahead/behind visible area
|
|
9
|
+
PREFETCH_LINES = 50
|
|
10
|
+
|
|
11
|
+
def initialize(lexer)
|
|
12
|
+
@lexer = lexer
|
|
13
|
+
@cache = {} # row => { tokens:, line_hash:, state_after: }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get tokens for a specific line, using cache when available
|
|
17
|
+
# Only tokenizes the requested line - does NOT pre-compute previous lines
|
|
18
|
+
def tokens_for(row, line, _buffer_lines)
|
|
19
|
+
line_hash = line.hash
|
|
20
|
+
|
|
21
|
+
# Check cache validity (single hash lookup)
|
|
22
|
+
cached = @cache[row]
|
|
23
|
+
return cached[:tokens] if cached && cached[:line_hash] == line_hash
|
|
24
|
+
|
|
25
|
+
# Get state from previous line's cache (if available)
|
|
26
|
+
# If not available, assume nil (no multiline state)
|
|
27
|
+
# This trades accuracy for performance - multiline constructs
|
|
28
|
+
# may not highlight correctly until user scrolls through the file
|
|
29
|
+
prev_cached = @cache[row - 1]
|
|
30
|
+
state_before = prev_cached&.dig(:state_after)
|
|
31
|
+
|
|
32
|
+
# Tokenize this line
|
|
33
|
+
tokens, state_after = @lexer.tokenize(line, state_before)
|
|
34
|
+
|
|
35
|
+
# Store in cache
|
|
36
|
+
@cache[row] = {
|
|
37
|
+
tokens:,
|
|
38
|
+
line_hash:,
|
|
39
|
+
state_after:
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tokens
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Prefetch tokens for lines around the visible area
|
|
46
|
+
def prefetch(visible_start, visible_end, buffer_lines)
|
|
47
|
+
return if buffer_lines.empty?
|
|
48
|
+
|
|
49
|
+
# Calculate prefetch range
|
|
50
|
+
prefetch_start = [visible_start - PREFETCH_LINES, 0].max
|
|
51
|
+
prefetch_end = [visible_end + PREFETCH_LINES, buffer_lines.length - 1].min
|
|
52
|
+
|
|
53
|
+
# Tokenize lines that aren't cached yet
|
|
54
|
+
(prefetch_start..prefetch_end).each do |row|
|
|
55
|
+
line = buffer_lines[row]
|
|
56
|
+
next if line.nil?
|
|
57
|
+
|
|
58
|
+
line_hash = line.hash
|
|
59
|
+
cached = @cache[row]
|
|
60
|
+
next if cached && cached[:line_hash] == line_hash
|
|
61
|
+
|
|
62
|
+
prev_cached = @cache[row - 1]
|
|
63
|
+
state_before = prev_cached&.dig(:state_after)
|
|
64
|
+
tokens, state_after = @lexer.tokenize(line, state_before)
|
|
65
|
+
|
|
66
|
+
@cache[row] = {
|
|
67
|
+
tokens:,
|
|
68
|
+
line_hash:,
|
|
69
|
+
state_after:
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Invalidate cache from a specific row onwards
|
|
75
|
+
# This is called when a line is modified
|
|
76
|
+
def invalidate(from_row)
|
|
77
|
+
@cache.delete_if { |row, _| row >= from_row }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Clear the entire cache
|
|
81
|
+
def clear
|
|
82
|
+
@cache.clear
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if a row has cached data
|
|
86
|
+
def cached?(row)
|
|
87
|
+
@cache.key?(row)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Renders the tab bar at the top of the screen
|
|
5
|
+
class TabBarRenderer
|
|
6
|
+
TAB_BAR_HEIGHT = 1
|
|
7
|
+
SEPARATOR_HEIGHT = 1
|
|
8
|
+
TAB_SEPARATOR = " | "
|
|
9
|
+
SEPARATOR_CHAR = "─"
|
|
10
|
+
|
|
11
|
+
def initialize(tab_manager, color_scheme: nil)
|
|
12
|
+
@tab_manager = tab_manager
|
|
13
|
+
@color_scheme = color_scheme
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(screen, row = 0)
|
|
17
|
+
return unless should_render?
|
|
18
|
+
|
|
19
|
+
render_tabs(screen, row)
|
|
20
|
+
render_separator_line(screen, row + TAB_BAR_HEIGHT)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def height
|
|
24
|
+
should_render? ? TAB_BAR_HEIGHT + SEPARATOR_HEIGHT : 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def should_render?
|
|
30
|
+
@tab_manager.tab_count > 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_tabs(screen, row)
|
|
34
|
+
col = 0
|
|
35
|
+
|
|
36
|
+
@tab_manager.tabs.each_with_index do |tab, i|
|
|
37
|
+
# Add separator between tabs
|
|
38
|
+
if i.positive?
|
|
39
|
+
screen.put_with_style(row, col, TAB_SEPARATOR, tab_bar_style)
|
|
40
|
+
col += TAB_SEPARATOR.length
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Render tab with appropriate style
|
|
44
|
+
tab_text = build_tab_text(tab, i)
|
|
45
|
+
style = i == @tab_manager.current_index ? tab_bar_active_style : tab_bar_style
|
|
46
|
+
screen.put_with_style(row, col, tab_text, style)
|
|
47
|
+
col += tab_text.length
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Fill remaining space with tab bar style
|
|
51
|
+
remaining = screen.width - col
|
|
52
|
+
screen.put_with_style(row, col, " " * remaining, tab_bar_style) if remaining.positive?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_separator_line(screen, row)
|
|
56
|
+
separator_line = SEPARATOR_CHAR * screen.width
|
|
57
|
+
screen.put_with_style(row, 0, separator_line, separator_style)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_tab_text(tab, index)
|
|
61
|
+
marker = index == @tab_manager.current_index ? "*" : " "
|
|
62
|
+
"#{marker}#{index + 1}:#{truncate_name(tab.display_name, 15)}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def truncate_name(name, max_length)
|
|
66
|
+
return name if name.length <= max_length
|
|
67
|
+
|
|
68
|
+
"#{name[0, max_length - 1]}~"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tab_bar_style
|
|
72
|
+
@color_scheme&.[](:tab_bar) || @color_scheme&.[](:status_line) || default_style
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def tab_bar_active_style
|
|
76
|
+
@color_scheme&.[](:tab_bar_active) || @color_scheme&.[](:status_line_mode) || default_style
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def separator_style
|
|
80
|
+
@color_scheme&.[](:separator) || @color_scheme&.[](:status_line) || default_style
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def default_style
|
|
84
|
+
{ fg: :white, bg: :blue, bold: false, underline: false }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Manages multiple tab pages
|
|
5
|
+
# Each tab page has its own window layout
|
|
6
|
+
class TabManager
|
|
7
|
+
attr_reader :tabs, :current_index
|
|
8
|
+
|
|
9
|
+
def initialize(screen, color_scheme: nil)
|
|
10
|
+
@screen = screen
|
|
11
|
+
@color_scheme = color_scheme
|
|
12
|
+
@tabs = []
|
|
13
|
+
@current_index = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def current_tab
|
|
17
|
+
@tabs[@current_index]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add(tab = nil)
|
|
21
|
+
tab ||= TabPage.new(@screen, color_scheme: @color_scheme)
|
|
22
|
+
@tabs << tab
|
|
23
|
+
@current_index = @tabs.size - 1
|
|
24
|
+
tab
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rubocop:disable Naming/PredicateMethod
|
|
28
|
+
def close_current
|
|
29
|
+
return false if single_tab?
|
|
30
|
+
|
|
31
|
+
@tabs.delete_at(@current_index)
|
|
32
|
+
@current_index = [@current_index, @tabs.size - 1].min
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
# rubocop:enable Naming/PredicateMethod
|
|
36
|
+
|
|
37
|
+
def next_tab
|
|
38
|
+
return if @tabs.empty?
|
|
39
|
+
|
|
40
|
+
@current_index = (@current_index + 1) % @tabs.size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def prev_tab
|
|
44
|
+
return if @tabs.empty?
|
|
45
|
+
|
|
46
|
+
@current_index = (@current_index - 1) % @tabs.size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def first_tab
|
|
50
|
+
return if @tabs.empty?
|
|
51
|
+
|
|
52
|
+
@current_index = 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def last_tab
|
|
56
|
+
return if @tabs.empty?
|
|
57
|
+
|
|
58
|
+
@current_index = @tabs.size - 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# rubocop:disable Naming/PredicateMethod
|
|
62
|
+
def go_to(index)
|
|
63
|
+
return false if index.negative? || index >= @tabs.size
|
|
64
|
+
|
|
65
|
+
@current_index = index
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def move_tab(position)
|
|
70
|
+
return false if @tabs.size <= 1
|
|
71
|
+
|
|
72
|
+
tab = @tabs.delete_at(@current_index)
|
|
73
|
+
new_position = position.clamp(0, @tabs.size)
|
|
74
|
+
@tabs.insert(new_position, tab)
|
|
75
|
+
@current_index = new_position
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Naming/PredicateMethod
|
|
79
|
+
|
|
80
|
+
def tab_count
|
|
81
|
+
@tabs.size
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def single_tab?
|
|
85
|
+
@tabs.size <= 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def window_manager
|
|
89
|
+
current_tab&.window_manager
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def active_window
|
|
93
|
+
current_tab&.active_window
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/mui/tab_page.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Represents a single tab page containing a window manager
|
|
5
|
+
# Each tab page has its own independent window layout
|
|
6
|
+
class TabPage
|
|
7
|
+
attr_reader :window_manager
|
|
8
|
+
attr_accessor :name
|
|
9
|
+
|
|
10
|
+
def initialize(screen, color_scheme: nil, name: nil)
|
|
11
|
+
@window_manager = WindowManager.new(screen, color_scheme:)
|
|
12
|
+
@name = name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def active_window
|
|
16
|
+
@window_manager.active_window
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def layout_root
|
|
20
|
+
@window_manager.layout_root
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def windows
|
|
24
|
+
@window_manager.windows
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def window_count
|
|
28
|
+
@window_manager.window_count
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def display_name
|
|
32
|
+
@name || active_window&.buffer&.name || "[No Name]"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|