mui 0.1.0 → 0.2.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 +158 -0
- data/CHANGELOG.md +349 -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 +21 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_line.rb +137 -0
- data/lib/mui/command_registry.rb +25 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +56 -0
- data/lib/mui/editor.rb +319 -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 +101 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +26 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +105 -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 +100 -0
- data/lib/mui/key_handler/command_mode.rb +443 -0
- data/lib/mui/key_handler/insert_mode.rb +354 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +579 -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 +113 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +188 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +397 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -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 +122 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +185 -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 +85 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +88 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +74 -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/ruby_lexer.rb +114 -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 +162 -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 +158 -0
- data/lib/mui/window_manager.rb +249 -0
- data/lib/mui.rb +156 -2
- metadata +98 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
# Base class for language-specific lexers
|
|
6
|
+
# Subclasses should override token_patterns and optionally handle_multiline_state
|
|
7
|
+
class LexerBase
|
|
8
|
+
# Tokenize a single line of text
|
|
9
|
+
# TODO: Refactor to reduce complexity (Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity)
|
|
10
|
+
def tokenize(line, state = nil)
|
|
11
|
+
tokens = []
|
|
12
|
+
pos = 0
|
|
13
|
+
current_state = state
|
|
14
|
+
|
|
15
|
+
while pos < line.length
|
|
16
|
+
# Handle multiline state first (e.g., inside block comment)
|
|
17
|
+
if current_state
|
|
18
|
+
token, new_state, new_pos = handle_multiline_state(line, pos, current_state)
|
|
19
|
+
if token
|
|
20
|
+
tokens << token
|
|
21
|
+
pos = new_pos
|
|
22
|
+
current_state = new_state
|
|
23
|
+
next
|
|
24
|
+
elsif new_state.nil?
|
|
25
|
+
# State ended, continue normal tokenization
|
|
26
|
+
current_state = nil
|
|
27
|
+
pos = new_pos
|
|
28
|
+
next
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check for multiline state start
|
|
33
|
+
new_state, token, new_pos = check_multiline_start(line, pos)
|
|
34
|
+
if new_state
|
|
35
|
+
tokens << token if token
|
|
36
|
+
pos = new_pos
|
|
37
|
+
current_state = new_state
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Normal token matching
|
|
42
|
+
token = match_token(line, pos)
|
|
43
|
+
if token
|
|
44
|
+
tokens << token
|
|
45
|
+
pos = token.end_col + 1
|
|
46
|
+
else
|
|
47
|
+
# Skip unrecognized character
|
|
48
|
+
pos += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[tokens, current_state]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if a state continues to the next line
|
|
56
|
+
def continuing_state?(state)
|
|
57
|
+
!state.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
# Override in subclass to define token patterns
|
|
63
|
+
def token_patterns
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Override in subclass to handle multiline constructs
|
|
68
|
+
def handle_multiline_state(_line, pos, _state)
|
|
69
|
+
[nil, nil, pos]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Override in subclass to check for multiline construct starts
|
|
73
|
+
def check_multiline_start(_line, pos)
|
|
74
|
+
[nil, nil, pos]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Get compiled patterns (cached)
|
|
80
|
+
# Uses \G anchor for efficient matching at specific position
|
|
81
|
+
def compiled_patterns
|
|
82
|
+
@compiled_patterns ||= token_patterns.map do |type, pattern|
|
|
83
|
+
[type, /\G#{pattern}/]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def match_token(line, pos)
|
|
88
|
+
return nil if pos >= line.length
|
|
89
|
+
|
|
90
|
+
compiled_patterns.each do |type, pattern|
|
|
91
|
+
match = pattern.match(line, pos)
|
|
92
|
+
next unless match
|
|
93
|
+
|
|
94
|
+
text = match[0]
|
|
95
|
+
return Token.new(
|
|
96
|
+
type:,
|
|
97
|
+
start_col: pos,
|
|
98
|
+
end_col: pos + text.length - 1,
|
|
99
|
+
text:
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for C source code
|
|
7
|
+
class CLexer < LexerBase
|
|
8
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
9
|
+
# These are compiled once at class load time
|
|
10
|
+
COMPILED_PATTERNS = [
|
|
11
|
+
# Single line comment
|
|
12
|
+
[:comment, %r{\G//.*}],
|
|
13
|
+
# Single-line block comment /* ... */ on one line
|
|
14
|
+
[:comment, %r{\G/\*.*?\*/}],
|
|
15
|
+
# Double quoted string (with escape handling)
|
|
16
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
17
|
+
# Character literal
|
|
18
|
+
[:char, /\G'(?:[^'\\]|\\.)*'/],
|
|
19
|
+
# Preprocessor directives
|
|
20
|
+
[:preprocessor, /\G^\s*#\s*(?:include|define|undef|ifdef|ifndef|if|else|elif|endif|error|pragma|line)\b.*/],
|
|
21
|
+
# Float numbers (must be before integer)
|
|
22
|
+
[:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?[fFlL]?\b/i],
|
|
23
|
+
# Hexadecimal
|
|
24
|
+
[:number, /\G\b0x[0-9a-fA-F]+[uUlL]*\b/],
|
|
25
|
+
# Octal
|
|
26
|
+
[:number, /\G\b0[0-7]+[uUlL]*\b/],
|
|
27
|
+
# Integer
|
|
28
|
+
[:number, /\G\b\d+[uUlL]*\b/],
|
|
29
|
+
# Type keywords (int, char, void, etc.)
|
|
30
|
+
[:type, /\G\b(?:char|double|float|int|long|short|signed|unsigned|void|_Bool|_Complex|_Imaginary)\b/],
|
|
31
|
+
# Other keywords (if, for, return, const, static, etc.)
|
|
32
|
+
[:keyword, /\G\b(?:auto|break|case|const|continue|default|do|else|enum|extern|for|goto|if|register|return|sizeof|static|struct|switch|typedef|union|volatile|while|inline|restrict|_Alignas|_Alignof|_Atomic|_Generic|_Noreturn|_Static_assert|_Thread_local)\b/],
|
|
33
|
+
# Identifiers
|
|
34
|
+
[:identifier, /\G\b[a-zA-Z_][a-zA-Z0-9_]*\b/],
|
|
35
|
+
# Operators
|
|
36
|
+
[:operator, %r{\G(?:[+\-*/%&|^~<>=!]+|->|<<|>>|\+\+|--)}]
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# Multiline comment patterns (pre-compiled)
|
|
40
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
41
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
42
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
# Use pre-compiled class-level patterns
|
|
47
|
+
def compiled_patterns
|
|
48
|
+
COMPILED_PATTERNS
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Handle /* ... */ block comments that span multiple lines
|
|
52
|
+
def handle_multiline_state(line, pos, state)
|
|
53
|
+
return [nil, nil, pos] unless state == :block_comment
|
|
54
|
+
|
|
55
|
+
# Look for */
|
|
56
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
57
|
+
if end_match
|
|
58
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
59
|
+
text = line[pos..end_pos]
|
|
60
|
+
token = Token.new(
|
|
61
|
+
type: :comment,
|
|
62
|
+
start_col: pos,
|
|
63
|
+
end_col: end_pos,
|
|
64
|
+
text:
|
|
65
|
+
)
|
|
66
|
+
[token, nil, end_pos + 1]
|
|
67
|
+
else
|
|
68
|
+
# Entire line is part of block comment
|
|
69
|
+
text = line[pos..]
|
|
70
|
+
unless text.empty?
|
|
71
|
+
token = Token.new(
|
|
72
|
+
type: :comment,
|
|
73
|
+
start_col: pos,
|
|
74
|
+
end_col: line.length - 1,
|
|
75
|
+
text:
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
[token, :block_comment, line.length]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check for /* block comment start (that doesn't end on the same line)
|
|
83
|
+
def check_multiline_start(line, pos)
|
|
84
|
+
rest = line[pos..]
|
|
85
|
+
|
|
86
|
+
# Check for /* that doesn't have a matching */ on this line
|
|
87
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
88
|
+
return [nil, nil, pos] unless start_match
|
|
89
|
+
|
|
90
|
+
start_pos = pos + start_match.begin(0)
|
|
91
|
+
after_start = line[(start_pos + 2)..]
|
|
92
|
+
|
|
93
|
+
# Check if there's a closing */ on the same line after this /*
|
|
94
|
+
if after_start&.include?("*/")
|
|
95
|
+
# There's a closing on this line, let normal token matching handle it
|
|
96
|
+
[nil, nil, pos]
|
|
97
|
+
else
|
|
98
|
+
# No closing on this line, enter block comment state
|
|
99
|
+
text = line[start_pos..]
|
|
100
|
+
token = Token.new(
|
|
101
|
+
type: :comment,
|
|
102
|
+
start_col: start_pos,
|
|
103
|
+
end_col: line.length - 1,
|
|
104
|
+
text:
|
|
105
|
+
)
|
|
106
|
+
[:block_comment, token, line.length]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def match_token(line, pos)
|
|
113
|
+
# First check for start of multiline comment
|
|
114
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
115
|
+
rest = line[(pos + 2)..]
|
|
116
|
+
unless rest&.include?("*/")
|
|
117
|
+
# This will be handled by check_multiline_start
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
super
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for Ruby source code
|
|
7
|
+
class RubyLexer < LexerBase
|
|
8
|
+
KEYWORDS = %w[
|
|
9
|
+
BEGIN END __ENCODING__ __END__ __FILE__ __LINE__
|
|
10
|
+
alias and begin break case class def defined? do
|
|
11
|
+
else elsif end ensure false for if in module next
|
|
12
|
+
nil not or redo rescue retry return self super then
|
|
13
|
+
true undef unless until when while yield
|
|
14
|
+
require require_relative attr_reader attr_writer attr_accessor
|
|
15
|
+
private protected public include extend prepend
|
|
16
|
+
raise fail catch throw
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
20
|
+
# These are compiled once at class load time
|
|
21
|
+
COMPILED_PATTERNS = [
|
|
22
|
+
# Single line comment
|
|
23
|
+
[:comment, /\G#.*/],
|
|
24
|
+
# Double quoted string (with escape handling)
|
|
25
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
26
|
+
# Single quoted string (with escape handling)
|
|
27
|
+
[:string, /\G'(?:[^'\\]|\\.)*'/],
|
|
28
|
+
# Symbols
|
|
29
|
+
[:symbol, /\G:[a-zA-Z_][a-zA-Z0-9_]*[?!]?/],
|
|
30
|
+
# Float numbers (must be before integer)
|
|
31
|
+
[:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
|
|
32
|
+
# Hexadecimal
|
|
33
|
+
[:number, /\G\b0x[0-9a-f]+\b/i],
|
|
34
|
+
# Octal
|
|
35
|
+
[:number, /\G\b0o?[0-7]+\b/],
|
|
36
|
+
# Binary
|
|
37
|
+
[:number, /\G\b0b[01]+\b/i],
|
|
38
|
+
# Integer
|
|
39
|
+
[:number, /\G\b\d+\b/],
|
|
40
|
+
# Constants (capitalized identifiers)
|
|
41
|
+
[:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
|
|
42
|
+
# Keywords
|
|
43
|
+
[:keyword, /\G\b(?:BEGIN|END|__ENCODING__|__END__|__FILE__|__LINE__|alias|and|begin|break|case|class|def|defined\?|do|else|elsif|end|ensure|false|for|if|in|module|next|nil|not|or|redo|rescue|retry|return|self|super|then|true|undef|unless|until|when|while|yield|require|require_relative|attr_reader|attr_writer|attr_accessor|private|protected|public|include|extend|prepend|raise|fail|catch|throw)\b/],
|
|
44
|
+
# Instance variables (@foo, @@foo)
|
|
45
|
+
[:instance_variable, /\G@{1,2}[a-zA-Z_][a-zA-Z0-9_]*/],
|
|
46
|
+
# Global variables
|
|
47
|
+
[:global_variable, /\G\$[a-zA-Z_][a-zA-Z0-9_]*/],
|
|
48
|
+
# Method calls (.to_i, .each, .map!, .empty?, etc.)
|
|
49
|
+
[:method_call, /\G\.[a-z_][a-zA-Z0-9_]*[?!]?/],
|
|
50
|
+
# Identifiers (including method names with ? or !)
|
|
51
|
+
[:identifier, /\G\b[a-z_][a-zA-Z0-9_]*[?!]?/],
|
|
52
|
+
# Operators
|
|
53
|
+
[:operator, %r{\G(?:[+\-*/%&|^~<>=!]+|<<|>>|\*\*)}]
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
# Multiline patterns (also pre-compiled)
|
|
57
|
+
BLOCK_COMMENT_END = /\A=end\b/
|
|
58
|
+
BLOCK_COMMENT_START = /\A=begin\b/
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
# Use pre-compiled class-level patterns
|
|
63
|
+
def compiled_patterns
|
|
64
|
+
COMPILED_PATTERNS
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Handle =begin...=end block comments
|
|
68
|
+
def handle_multiline_state(line, pos, state)
|
|
69
|
+
return [nil, nil, pos] unless state == :block_comment
|
|
70
|
+
|
|
71
|
+
# Check for =end
|
|
72
|
+
text = line[pos..]
|
|
73
|
+
if BLOCK_COMMENT_END.match?(text)
|
|
74
|
+
token = Token.new(
|
|
75
|
+
type: :comment,
|
|
76
|
+
start_col: pos,
|
|
77
|
+
end_col: line.length - 1,
|
|
78
|
+
text:
|
|
79
|
+
)
|
|
80
|
+
[token, nil, line.length]
|
|
81
|
+
else
|
|
82
|
+
# Entire line is part of block comment
|
|
83
|
+
unless text.empty?
|
|
84
|
+
token = Token.new(
|
|
85
|
+
type: :comment,
|
|
86
|
+
start_col: pos,
|
|
87
|
+
end_col: line.length - 1,
|
|
88
|
+
text:
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
[token, :block_comment, line.length]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check for =begin block comment start
|
|
96
|
+
def check_multiline_start(line, pos)
|
|
97
|
+
return [nil, nil, pos] unless pos.zero?
|
|
98
|
+
|
|
99
|
+
if BLOCK_COMMENT_START.match?(line)
|
|
100
|
+
token = Token.new(
|
|
101
|
+
type: :comment,
|
|
102
|
+
start_col: 0,
|
|
103
|
+
end_col: line.length - 1,
|
|
104
|
+
text: line
|
|
105
|
+
)
|
|
106
|
+
[:block_comment, token, line.length]
|
|
107
|
+
else
|
|
108
|
+
[nil, nil, pos]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -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
|