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,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module HandlerResult
|
|
5
|
+
# Base class for handler results with common attributes and default implementations
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :mode, :message
|
|
8
|
+
|
|
9
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false)
|
|
10
|
+
@mode = mode
|
|
11
|
+
@message = message
|
|
12
|
+
@quit = quit
|
|
13
|
+
@pending_sequence = pending_sequence
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def quit?
|
|
18
|
+
@quit
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# True when waiting for more keys in a multi-key sequence
|
|
22
|
+
def pending_sequence?
|
|
23
|
+
@pending_sequence
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start_selection?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def line_mode?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear_selection?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def toggle_line_mode?
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Result for NormalMode - handles visual mode start
|
|
44
|
+
class NormalModeResult < Base
|
|
45
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, start_selection: false, line_mode: false, group_started: false)
|
|
46
|
+
@start_selection = start_selection
|
|
47
|
+
@line_mode = line_mode
|
|
48
|
+
@group_started = group_started
|
|
49
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def start_selection?
|
|
53
|
+
@start_selection
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def line_mode?
|
|
57
|
+
@line_mode
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def group_started?
|
|
61
|
+
@group_started
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Result for VisualMode - handles selection clear and line mode toggle
|
|
66
|
+
class VisualModeResult < Base
|
|
67
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, clear_selection: false, toggle_line_mode: false, group_started: false)
|
|
68
|
+
@clear_selection = clear_selection
|
|
69
|
+
@toggle_line_mode = toggle_line_mode
|
|
70
|
+
@group_started = group_started
|
|
71
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clear_selection?
|
|
75
|
+
@clear_selection
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def toggle_line_mode?
|
|
79
|
+
@toggle_line_mode
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def group_started?
|
|
83
|
+
@group_started
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Result for InsertMode - uses base functionality only
|
|
88
|
+
class InsertModeResult < Base
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Result for CommandMode - uses base functionality only
|
|
92
|
+
class CommandModeResult < Base
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Result for SearchMode - handles search execution
|
|
96
|
+
class SearchModeResult < Base
|
|
97
|
+
def initialize(mode: nil, message: nil, quit: false, pending_sequence: false, cancelled: false)
|
|
98
|
+
@cancelled = cancelled
|
|
99
|
+
super(mode:, message:, quit:, pending_sequence:)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cancelled?
|
|
103
|
+
@cancelled
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
class Highlight
|
|
5
|
+
attr_reader :start_col, :end_col, :style, :priority
|
|
6
|
+
|
|
7
|
+
def initialize(start_col:, end_col:, style:, priority:)
|
|
8
|
+
@start_col = start_col
|
|
9
|
+
@end_col = end_col
|
|
10
|
+
@style = style
|
|
11
|
+
@priority = priority
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def overlaps?(other)
|
|
15
|
+
start_col <= other.end_col && end_col >= other.start_col
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def <=>(other)
|
|
19
|
+
[start_col, -priority] <=> [other.start_col, -other.priority]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Highlighters
|
|
5
|
+
class Base
|
|
6
|
+
PRIORITY_SYNTAX = 100
|
|
7
|
+
PRIORITY_SELECTION = 200
|
|
8
|
+
PRIORITY_SEARCH = 300
|
|
9
|
+
|
|
10
|
+
def initialize(color_scheme)
|
|
11
|
+
@color_scheme = color_scheme
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def highlights_for(_row, _line, _options = {})
|
|
15
|
+
raise Mui::MethodNotOverriddenError, "#{self.class}#highlights_for must be implemented"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def priority
|
|
19
|
+
0
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Highlighters
|
|
5
|
+
class SearchHighlighter < Base
|
|
6
|
+
def highlights_for(row, _line, options = {})
|
|
7
|
+
search_state = options[:search_state]
|
|
8
|
+
buffer = options[:buffer]
|
|
9
|
+
return [] unless search_state&.has_pattern?
|
|
10
|
+
|
|
11
|
+
matches = search_state.matches_for_row(row, buffer:)
|
|
12
|
+
matches.map do |match|
|
|
13
|
+
Highlight.new(
|
|
14
|
+
start_col: match[:col],
|
|
15
|
+
end_col: match[:end_col],
|
|
16
|
+
style: :search_highlight,
|
|
17
|
+
priority:
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def priority
|
|
23
|
+
PRIORITY_SEARCH
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Highlighters
|
|
5
|
+
class SelectionHighlighter < Base
|
|
6
|
+
def highlights_for(row, line, options = {})
|
|
7
|
+
selection = options[:selection]
|
|
8
|
+
return [] unless selection
|
|
9
|
+
|
|
10
|
+
range = selection.normalized_range
|
|
11
|
+
return [] if row < range[:start_row] || row > range[:end_row]
|
|
12
|
+
|
|
13
|
+
if selection.line_mode
|
|
14
|
+
line_mode_highlights(line)
|
|
15
|
+
else
|
|
16
|
+
char_mode_highlights(row, line, range)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def priority
|
|
21
|
+
PRIORITY_SELECTION
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def line_mode_highlights(line)
|
|
27
|
+
[Highlight.new(
|
|
28
|
+
start_col: 0,
|
|
29
|
+
end_col: [line.length - 1, 0].max,
|
|
30
|
+
style: :visual_selection,
|
|
31
|
+
priority:
|
|
32
|
+
)]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def char_mode_highlights(row, line, range)
|
|
36
|
+
start_col = row == range[:start_row] ? range[:start_col] : 0
|
|
37
|
+
end_col = row == range[:end_row] ? range[:end_col] : [line.length - 1, 0].max
|
|
38
|
+
|
|
39
|
+
[Highlight.new(
|
|
40
|
+
start_col:,
|
|
41
|
+
end_col:,
|
|
42
|
+
style: :visual_selection,
|
|
43
|
+
priority:
|
|
44
|
+
)]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Highlighters
|
|
5
|
+
# Provides syntax highlighting based on language-specific lexers
|
|
6
|
+
class SyntaxHighlighter < Base
|
|
7
|
+
# Maps token types to ColorScheme style names
|
|
8
|
+
# Note: identifier and operator are excluded to reduce highlight count
|
|
9
|
+
# (they typically use the same color as normal text)
|
|
10
|
+
TOKEN_STYLE_MAP = {
|
|
11
|
+
keyword: :syntax_keyword,
|
|
12
|
+
string: :syntax_string,
|
|
13
|
+
comment: :syntax_comment,
|
|
14
|
+
number: :syntax_number,
|
|
15
|
+
symbol: :syntax_symbol,
|
|
16
|
+
constant: :syntax_constant,
|
|
17
|
+
preprocessor: :syntax_preprocessor,
|
|
18
|
+
char: :syntax_string,
|
|
19
|
+
instance_variable: :syntax_instance_variable,
|
|
20
|
+
global_variable: :syntax_global_variable,
|
|
21
|
+
method_call: :syntax_method_call,
|
|
22
|
+
type: :syntax_type,
|
|
23
|
+
macro: :syntax_keyword, # Rust macros (println!, vec!, etc.)
|
|
24
|
+
regex: :syntax_string # JavaScript/TypeScript regex literals
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(color_scheme, buffer: nil)
|
|
28
|
+
super(color_scheme)
|
|
29
|
+
@buffer = buffer
|
|
30
|
+
setup_lexer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Update the buffer and reset the lexer
|
|
34
|
+
def buffer=(new_buffer)
|
|
35
|
+
@buffer = new_buffer
|
|
36
|
+
setup_lexer
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate highlights for a line
|
|
40
|
+
# TODO: Refactor to reduce complexity (Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity)
|
|
41
|
+
def highlights_for(row, line, options = {})
|
|
42
|
+
return [] unless @lexer
|
|
43
|
+
return [] unless Mui.config.get(:syntax)
|
|
44
|
+
|
|
45
|
+
buffer_lines = options[:buffer]&.lines || @buffer&.lines || []
|
|
46
|
+
tokens = @token_cache.tokens_for(row, line, buffer_lines)
|
|
47
|
+
|
|
48
|
+
tokens.filter_map do |token|
|
|
49
|
+
style = style_for_token_type(token.type)
|
|
50
|
+
next unless style && @color_scheme[style]
|
|
51
|
+
|
|
52
|
+
Highlight.new(
|
|
53
|
+
start_col: token.start_col,
|
|
54
|
+
end_col: token.end_col,
|
|
55
|
+
style:,
|
|
56
|
+
priority:
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def priority
|
|
62
|
+
PRIORITY_SYNTAX
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Invalidate cache from a specific row onwards
|
|
66
|
+
# Called when buffer content changes
|
|
67
|
+
def invalidate_from(row)
|
|
68
|
+
@token_cache&.invalidate(row)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Clear the entire cache
|
|
72
|
+
def clear_cache
|
|
73
|
+
@token_cache&.clear
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Prefetch tokens for lines around the visible area
|
|
77
|
+
def prefetch(visible_start, visible_end)
|
|
78
|
+
return unless @lexer && @token_cache && @buffer
|
|
79
|
+
return unless Mui.config.get(:syntax)
|
|
80
|
+
|
|
81
|
+
@token_cache.prefetch(visible_start, visible_end, @buffer.lines)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if this highlighter is active (has a lexer)
|
|
85
|
+
def active?
|
|
86
|
+
!@lexer.nil?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def setup_lexer
|
|
92
|
+
@lexer = nil
|
|
93
|
+
@token_cache = nil
|
|
94
|
+
return unless @buffer
|
|
95
|
+
|
|
96
|
+
@lexer = Syntax::LanguageDetector.lexer_for_file(@buffer.name)
|
|
97
|
+
return unless @lexer
|
|
98
|
+
|
|
99
|
+
@token_cache = Syntax::TokenCache.new(@lexer)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def style_for_token_type(type)
|
|
103
|
+
TOKEN_STYLE_MAP[type]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/mui/input.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Renders completion popup for Insert mode (LSP completions)
|
|
5
|
+
class InsertCompletionRenderer
|
|
6
|
+
MAX_VISIBLE_ITEMS = 10
|
|
7
|
+
|
|
8
|
+
def initialize(screen, color_scheme)
|
|
9
|
+
@screen = screen
|
|
10
|
+
@color_scheme = color_scheme
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(completion_state, cursor_row, cursor_col)
|
|
14
|
+
return unless completion_state.active?
|
|
15
|
+
|
|
16
|
+
items = completion_state.items
|
|
17
|
+
selected_index = completion_state.selected_index
|
|
18
|
+
|
|
19
|
+
# Calculate visible window
|
|
20
|
+
visible_start, visible_end = calculate_visible_range(items.length, selected_index)
|
|
21
|
+
visible_items = items[visible_start...visible_end]
|
|
22
|
+
|
|
23
|
+
# Calculate popup dimensions
|
|
24
|
+
max_width = calculate_max_width(visible_items)
|
|
25
|
+
popup_height = visible_items.length
|
|
26
|
+
|
|
27
|
+
# Calculate position (popup appears below the cursor)
|
|
28
|
+
popup_row = cursor_row + 1
|
|
29
|
+
popup_col = cursor_col
|
|
30
|
+
|
|
31
|
+
# If popup would go below screen, show above cursor instead
|
|
32
|
+
popup_row = cursor_row - popup_height if popup_row + popup_height > @screen.height - 1
|
|
33
|
+
|
|
34
|
+
# Ensure popup stays within screen bounds
|
|
35
|
+
popup_col = [@screen.width - max_width - 1, popup_col].min
|
|
36
|
+
popup_col = [0, popup_col].max
|
|
37
|
+
popup_row = [0, popup_row].max
|
|
38
|
+
|
|
39
|
+
# Render each visible item
|
|
40
|
+
visible_items.each_with_index do |item, i|
|
|
41
|
+
actual_index = visible_start + i
|
|
42
|
+
is_selected = actual_index == selected_index
|
|
43
|
+
|
|
44
|
+
render_item(
|
|
45
|
+
item,
|
|
46
|
+
popup_row + i,
|
|
47
|
+
popup_col,
|
|
48
|
+
max_width,
|
|
49
|
+
is_selected
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def calculate_visible_range(total_count, selected_index)
|
|
57
|
+
return [0, total_count] if total_count <= MAX_VISIBLE_ITEMS
|
|
58
|
+
|
|
59
|
+
# Try to center the selected item
|
|
60
|
+
half = MAX_VISIBLE_ITEMS / 2
|
|
61
|
+
start_index = selected_index - half
|
|
62
|
+
start_index = [0, start_index].max
|
|
63
|
+
end_index = start_index + MAX_VISIBLE_ITEMS
|
|
64
|
+
end_index = [total_count, end_index].min
|
|
65
|
+
start_index = end_index - MAX_VISIBLE_ITEMS
|
|
66
|
+
|
|
67
|
+
[start_index, end_index]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def calculate_max_width(items)
|
|
71
|
+
return 0 if items.empty?
|
|
72
|
+
|
|
73
|
+
items.map { |item| display_width(item_label(item)) }.max + 2 # +2 for padding
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def item_label(item)
|
|
77
|
+
item[:label] || item.to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def display_width(text)
|
|
81
|
+
UnicodeWidth.string_width(text)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_item(item, row, col, width, selected)
|
|
85
|
+
style_key = selected ? :completion_popup_selected : :completion_popup
|
|
86
|
+
style = @color_scheme[style_key]
|
|
87
|
+
label = item_label(item)
|
|
88
|
+
padded_text = " #{label}".ljust(width)
|
|
89
|
+
@screen.put_with_style(row, col, padded_text, style)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Manages Insert mode completion state for LSP completions
|
|
5
|
+
class InsertCompletionState
|
|
6
|
+
attr_reader :items, :selected_index, :prefix, :original_items
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
reset
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reset
|
|
13
|
+
@items = []
|
|
14
|
+
@original_items = []
|
|
15
|
+
@selected_index = 0
|
|
16
|
+
@prefix = ""
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def active?
|
|
20
|
+
!@items.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start(items, prefix: "")
|
|
24
|
+
@original_items = items.dup
|
|
25
|
+
@items = items
|
|
26
|
+
@selected_index = 0
|
|
27
|
+
@prefix = prefix
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Update prefix and filter items based on new prefix
|
|
31
|
+
def update_prefix(new_prefix)
|
|
32
|
+
return if new_prefix == @prefix
|
|
33
|
+
|
|
34
|
+
@prefix = new_prefix
|
|
35
|
+
@items = @original_items.select do |item|
|
|
36
|
+
label = item[:label] || item[:insert_text] || ""
|
|
37
|
+
label.downcase.start_with?(new_prefix.downcase)
|
|
38
|
+
end
|
|
39
|
+
@selected_index = 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def select_next
|
|
43
|
+
return unless active?
|
|
44
|
+
|
|
45
|
+
@selected_index = (@selected_index + 1) % @items.length
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def select_previous
|
|
49
|
+
return unless active?
|
|
50
|
+
|
|
51
|
+
@selected_index = (@selected_index - 1) % @items.length
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def current_item
|
|
55
|
+
return nil unless active?
|
|
56
|
+
|
|
57
|
+
@items[@selected_index]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the text to insert for the current item
|
|
61
|
+
def insert_text
|
|
62
|
+
return nil unless current_item
|
|
63
|
+
|
|
64
|
+
item = current_item
|
|
65
|
+
# Prefer textEdit.newText if available (text_edit value has string keys from LSP)
|
|
66
|
+
item.dig(:text_edit, "newText") || item[:insert_text] || item[:label] || item.to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns the textEdit range if available
|
|
70
|
+
def text_edit_range
|
|
71
|
+
return nil unless current_item
|
|
72
|
+
|
|
73
|
+
# text_edit value has string keys from LSP
|
|
74
|
+
current_item.dig(:text_edit, "range")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/mui/job.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Represents a single asynchronous job
|
|
5
|
+
class Job
|
|
6
|
+
# Job status constants
|
|
7
|
+
module Status
|
|
8
|
+
PENDING = :pending
|
|
9
|
+
RUNNING = :running
|
|
10
|
+
COMPLETED = :completed
|
|
11
|
+
FAILED = :failed
|
|
12
|
+
CANCELLED = :cancelled
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :id, :status, :result, :error
|
|
16
|
+
|
|
17
|
+
def initialize(id, &block)
|
|
18
|
+
@id = id
|
|
19
|
+
@block = block
|
|
20
|
+
@status = Status::PENDING
|
|
21
|
+
@result = nil
|
|
22
|
+
@error = nil
|
|
23
|
+
@cancelled = false
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
return if @cancelled
|
|
30
|
+
|
|
31
|
+
@status = Status::RUNNING
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
@result = @block.call
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@status = @cancelled ? Status::CANCELLED : Status::COMPLETED
|
|
38
|
+
end
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@error = e
|
|
42
|
+
@status = Status::FAILED
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cancel
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
return false if @status == Status::COMPLETED || @status == Status::FAILED
|
|
50
|
+
|
|
51
|
+
@cancelled = true
|
|
52
|
+
@status = Status::CANCELLED if @status == Status::PENDING
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pending?
|
|
58
|
+
@status == Status::PENDING
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def running?
|
|
62
|
+
@status == Status::RUNNING
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def completed?
|
|
66
|
+
@status == Status::COMPLETED
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def failed?
|
|
70
|
+
@status == Status::FAILED
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cancelled?
|
|
74
|
+
@status == Status::CANCELLED
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def finished?
|
|
78
|
+
completed? || failed? || cancelled?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|