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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +13 -8
  3. data/CHANGELOG.md +99 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -0
  7. data/docs/getting-started.md +140 -0
  8. data/docs/index.md +55 -0
  9. data/docs/jobs.md +297 -0
  10. data/docs/keybindings.md +229 -0
  11. data/docs/plugins.md +285 -0
  12. data/docs/syntax-highlighting.md +149 -0
  13. data/lib/mui/command_completer.rb +11 -2
  14. data/lib/mui/command_history.rb +89 -0
  15. data/lib/mui/command_line.rb +32 -2
  16. data/lib/mui/command_registry.rb +21 -2
  17. data/lib/mui/config.rb +3 -1
  18. data/lib/mui/editor.rb +78 -2
  19. data/lib/mui/handler_result.rb +13 -7
  20. data/lib/mui/highlighters/search_highlighter.rb +2 -1
  21. data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
  22. data/lib/mui/key_handler/base.rb +87 -0
  23. data/lib/mui/key_handler/command_mode.rb +68 -0
  24. data/lib/mui/key_handler/insert_mode.rb +10 -41
  25. data/lib/mui/key_handler/normal_mode.rb +24 -51
  26. data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
  27. data/lib/mui/key_handler/search_mode.rb +10 -7
  28. data/lib/mui/key_handler/visual_mode.rb +15 -10
  29. data/lib/mui/key_notation_parser.rb +152 -0
  30. data/lib/mui/key_sequence.rb +67 -0
  31. data/lib/mui/key_sequence_buffer.rb +85 -0
  32. data/lib/mui/key_sequence_handler.rb +163 -0
  33. data/lib/mui/key_sequence_matcher.rb +79 -0
  34. data/lib/mui/line_renderer.rb +52 -1
  35. data/lib/mui/mode_manager.rb +3 -2
  36. data/lib/mui/screen.rb +24 -6
  37. data/lib/mui/search_state.rb +61 -28
  38. data/lib/mui/syntax/language_detector.rb +33 -1
  39. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  40. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  41. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  42. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  43. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  44. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  45. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  46. data/lib/mui/terminal_adapter/curses.rb +13 -11
  47. data/lib/mui/version.rb +1 -1
  48. data/lib/mui/window.rb +83 -40
  49. data/lib/mui/window_manager.rb +7 -0
  50. data/lib/mui/wrap_cache.rb +40 -0
  51. data/lib/mui/wrap_helper.rb +84 -0
  52. data/lib/mui.rb +15 -0
  53. metadata +26 -3
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Syntax
5
+ module Lexers
6
+ # Lexer for Go source code
7
+ class GoLexer < LexerBase
8
+ # Go keywords
9
+ KEYWORDS = %w[
10
+ break case chan const continue default defer else fallthrough
11
+ for func go goto if import interface map package range return
12
+ select struct switch type var
13
+ ].freeze
14
+
15
+ # Go built-in types
16
+ TYPES = %w[
17
+ bool byte complex64 complex128 error float32 float64
18
+ int int8 int16 int32 int64 rune string
19
+ uint uint8 uint16 uint32 uint64 uintptr
20
+ any comparable
21
+ ].freeze
22
+
23
+ # Go constants
24
+ CONSTANTS = %w[true false nil iota].freeze
25
+
26
+ # Pre-compiled patterns with \G anchor for position-specific matching
27
+ COMPILED_PATTERNS = [
28
+ # Single line comment
29
+ [:comment, %r{\G//.*}],
30
+ # Single-line block comment /* ... */ on one line
31
+ [:comment, %r{\G/\*.*?\*/}],
32
+ # Raw string literal (backtick)
33
+ [:string, /\G`[^`]*`/],
34
+ # Double quoted string (with escape handling)
35
+ [:string, /\G"(?:[^"\\]|\\.)*"/],
36
+ # Character literal (rune)
37
+ [:char, /\G'(?:[^'\\]|\\.)*'/],
38
+ # Float numbers (must be before integer)
39
+ [:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
40
+ # Hexadecimal
41
+ [:number, /\G\b0x[0-9a-fA-F]+\b/i],
42
+ # Octal
43
+ [:number, /\G\b0o[0-7]+\b/i],
44
+ # Binary
45
+ [:number, /\G\b0b[01]+\b/i],
46
+ # Integer
47
+ [:number, /\G\b\d+\b/],
48
+ # Constants (true, false, nil, iota)
49
+ [:constant, /\G\b(?:true|false|nil|iota)\b/],
50
+ # Types
51
+ [:type, /\G\b(?:bool|byte|complex64|complex128|error|float32|float64|int|int8|int16|int32|int64|rune|string|uint|uint8|uint16|uint32|uint64|uintptr|any|comparable)\b/],
52
+ # Keywords
53
+ [:keyword, /\G\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/],
54
+ # Exported identifiers (start with uppercase)
55
+ [:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
56
+ # Regular identifiers
57
+ [:identifier, /\G\b[a-z_][a-zA-Z0-9_]*\b/],
58
+ # Operators
59
+ [:operator, %r{\G(?:&&|\|\||<-|<<=?|>>=?|&\^=?|[+\-*/%&|^<>=!]=?|:=|\+\+|--)}]
60
+ ].freeze
61
+
62
+ # Multiline comment patterns (pre-compiled)
63
+ BLOCK_COMMENT_END = %r{\*/}
64
+ BLOCK_COMMENT_START = %r{/\*}
65
+ BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
66
+
67
+ # Raw string patterns (pre-compiled)
68
+ RAW_STRING_START = /\A`/
69
+ RAW_STRING_END = /`/
70
+
71
+ protected
72
+
73
+ def compiled_patterns
74
+ COMPILED_PATTERNS
75
+ end
76
+
77
+ # Handle /* ... */ block comments and raw strings that span multiple lines
78
+ def handle_multiline_state(line, pos, state)
79
+ case state
80
+ when :block_comment
81
+ handle_block_comment(line, pos)
82
+ when :raw_string
83
+ handle_raw_string(line, pos)
84
+ else
85
+ [nil, nil, pos]
86
+ end
87
+ end
88
+
89
+ def check_multiline_start(line, pos)
90
+ rest = line[pos..]
91
+
92
+ # Check for raw string start
93
+ if rest.match?(RAW_STRING_START)
94
+ after_start = line[(pos + 1)..]
95
+ unless after_start&.include?("`")
96
+ # No closing on this line, enter raw string state
97
+ text = line[pos..]
98
+ token = Token.new(
99
+ type: :string,
100
+ start_col: pos,
101
+ end_col: line.length - 1,
102
+ text:
103
+ )
104
+ return [:raw_string, token, line.length]
105
+ end
106
+ end
107
+
108
+ # Check for /* that doesn't have a matching */ on this line
109
+ start_match = rest.match(BLOCK_COMMENT_START)
110
+ return [nil, nil, pos] unless start_match
111
+
112
+ start_pos = pos + start_match.begin(0)
113
+ after_start = line[(start_pos + 2)..]
114
+
115
+ if after_start&.include?("*/")
116
+ [nil, nil, pos]
117
+ else
118
+ text = line[start_pos..]
119
+ token = Token.new(
120
+ type: :comment,
121
+ start_col: start_pos,
122
+ end_col: line.length - 1,
123
+ text:
124
+ )
125
+ [:block_comment, token, line.length]
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def handle_block_comment(line, pos)
132
+ end_match = line[pos..].match(BLOCK_COMMENT_END)
133
+ if end_match
134
+ end_pos = pos + end_match.begin(0) + 1
135
+ text = line[pos..end_pos]
136
+ token = Token.new(
137
+ type: :comment,
138
+ start_col: pos,
139
+ end_col: end_pos,
140
+ text:
141
+ )
142
+ [token, nil, end_pos + 1]
143
+ else
144
+ text = line[pos..]
145
+ token = if text.empty?
146
+ nil
147
+ else
148
+ Token.new(
149
+ type: :comment,
150
+ start_col: pos,
151
+ end_col: line.length - 1,
152
+ text:
153
+ )
154
+ end
155
+ [token, :block_comment, line.length]
156
+ end
157
+ end
158
+
159
+ def handle_raw_string(line, pos)
160
+ end_match = line[pos..].match(RAW_STRING_END)
161
+ if end_match
162
+ end_pos = pos + end_match.begin(0)
163
+ text = line[pos..end_pos]
164
+ token = Token.new(
165
+ type: :string,
166
+ start_col: pos,
167
+ end_col: end_pos,
168
+ text:
169
+ )
170
+ [token, nil, end_pos + 1]
171
+ else
172
+ text = line[pos..]
173
+ token = if text.empty?
174
+ nil
175
+ else
176
+ Token.new(
177
+ type: :string,
178
+ start_col: pos,
179
+ end_col: line.length - 1,
180
+ text:
181
+ )
182
+ end
183
+ [token, :raw_string, line.length]
184
+ end
185
+ end
186
+
187
+ def match_token(line, pos)
188
+ # Check for start of raw string
189
+ if line[pos..].match?(RAW_STRING_START)
190
+ rest = line[(pos + 1)..]
191
+ return nil unless rest&.include?("`")
192
+ end
193
+
194
+ # Check for start of multiline comment
195
+ if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
196
+ rest = line[(pos + 2)..]
197
+ return nil unless rest&.include?("*/")
198
+ end
199
+
200
+ super
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -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