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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -0
  3. data/CHANGELOG.md +448 -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/exe/mui +1 -2
  14. data/lib/mui/autocmd.rb +66 -0
  15. data/lib/mui/buffer.rb +275 -0
  16. data/lib/mui/buffer_word_cache.rb +131 -0
  17. data/lib/mui/buffer_word_completer.rb +77 -0
  18. data/lib/mui/color_manager.rb +136 -0
  19. data/lib/mui/color_scheme.rb +63 -0
  20. data/lib/mui/command_completer.rb +30 -0
  21. data/lib/mui/command_context.rb +90 -0
  22. data/lib/mui/command_history.rb +89 -0
  23. data/lib/mui/command_line.rb +167 -0
  24. data/lib/mui/command_registry.rb +44 -0
  25. data/lib/mui/completion_renderer.rb +84 -0
  26. data/lib/mui/completion_state.rb +58 -0
  27. data/lib/mui/config.rb +58 -0
  28. data/lib/mui/editor.rb +395 -0
  29. data/lib/mui/error.rb +29 -0
  30. data/lib/mui/file_completer.rb +51 -0
  31. data/lib/mui/floating_window.rb +161 -0
  32. data/lib/mui/handler_result.rb +107 -0
  33. data/lib/mui/highlight.rb +22 -0
  34. data/lib/mui/highlighters/base.rb +23 -0
  35. data/lib/mui/highlighters/search_highlighter.rb +27 -0
  36. data/lib/mui/highlighters/selection_highlighter.rb +48 -0
  37. data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
  38. data/lib/mui/input.rb +17 -0
  39. data/lib/mui/insert_completion_renderer.rb +92 -0
  40. data/lib/mui/insert_completion_state.rb +77 -0
  41. data/lib/mui/job.rb +81 -0
  42. data/lib/mui/job_manager.rb +113 -0
  43. data/lib/mui/key_code.rb +30 -0
  44. data/lib/mui/key_handler/base.rb +187 -0
  45. data/lib/mui/key_handler/command_mode.rb +511 -0
  46. data/lib/mui/key_handler/insert_mode.rb +323 -0
  47. data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
  48. data/lib/mui/key_handler/normal_mode.rb +552 -0
  49. data/lib/mui/key_handler/operators/base_operator.rb +134 -0
  50. data/lib/mui/key_handler/operators/change_operator.rb +179 -0
  51. data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
  52. data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
  53. data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
  54. data/lib/mui/key_handler/search_mode.rb +191 -0
  55. data/lib/mui/key_handler/visual_line_mode.rb +20 -0
  56. data/lib/mui/key_handler/visual_mode.rb +402 -0
  57. data/lib/mui/key_handler/window_command.rb +112 -0
  58. data/lib/mui/key_handler.rb +16 -0
  59. data/lib/mui/key_notation_parser.rb +152 -0
  60. data/lib/mui/key_sequence.rb +67 -0
  61. data/lib/mui/key_sequence_buffer.rb +85 -0
  62. data/lib/mui/key_sequence_handler.rb +163 -0
  63. data/lib/mui/key_sequence_matcher.rb +79 -0
  64. data/lib/mui/layout/calculator.rb +15 -0
  65. data/lib/mui/layout/leaf_node.rb +33 -0
  66. data/lib/mui/layout/node.rb +29 -0
  67. data/lib/mui/layout/split_node.rb +132 -0
  68. data/lib/mui/line_renderer.rb +173 -0
  69. data/lib/mui/mode.rb +13 -0
  70. data/lib/mui/mode_manager.rb +186 -0
  71. data/lib/mui/motion.rb +139 -0
  72. data/lib/mui/plugin.rb +35 -0
  73. data/lib/mui/plugin_manager.rb +106 -0
  74. data/lib/mui/register.rb +110 -0
  75. data/lib/mui/screen.rb +103 -0
  76. data/lib/mui/search_completer.rb +50 -0
  77. data/lib/mui/search_input.rb +40 -0
  78. data/lib/mui/search_state.rb +121 -0
  79. data/lib/mui/selection.rb +55 -0
  80. data/lib/mui/status_line_renderer.rb +40 -0
  81. data/lib/mui/syntax/language_detector.rb +106 -0
  82. data/lib/mui/syntax/lexer_base.rb +106 -0
  83. data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
  84. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  85. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  86. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  87. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  88. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  89. data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
  90. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  91. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  92. data/lib/mui/syntax/token.rb +42 -0
  93. data/lib/mui/syntax/token_cache.rb +91 -0
  94. data/lib/mui/tab_bar_renderer.rb +87 -0
  95. data/lib/mui/tab_manager.rb +96 -0
  96. data/lib/mui/tab_page.rb +35 -0
  97. data/lib/mui/terminal_adapter/base.rb +92 -0
  98. data/lib/mui/terminal_adapter/curses.rb +164 -0
  99. data/lib/mui/terminal_adapter.rb +4 -0
  100. data/lib/mui/themes/default.rb +315 -0
  101. data/lib/mui/undo_manager.rb +83 -0
  102. data/lib/mui/undoable_action.rb +175 -0
  103. data/lib/mui/unicode_width.rb +100 -0
  104. data/lib/mui/version.rb +1 -1
  105. data/lib/mui/window.rb +201 -0
  106. data/lib/mui/window_manager.rb +256 -0
  107. data/lib/mui/wrap_cache.rb +40 -0
  108. data/lib/mui/wrap_helper.rb +84 -0
  109. data/lib/mui.rb +171 -2
  110. 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
@@ -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