mui 0.2.0 → 0.4.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +18 -10
  3. data/CHANGELOG.md +162 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +314 -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 +155 -0
  13. data/lib/mui/color_manager.rb +140 -6
  14. data/lib/mui/color_scheme.rb +1 -0
  15. data/lib/mui/command_completer.rb +11 -2
  16. data/lib/mui/command_history.rb +89 -0
  17. data/lib/mui/command_line.rb +32 -2
  18. data/lib/mui/command_registry.rb +21 -2
  19. data/lib/mui/config.rb +3 -1
  20. data/lib/mui/editor.rb +90 -2
  21. data/lib/mui/floating_window.rb +53 -1
  22. data/lib/mui/handler_result.rb +13 -7
  23. data/lib/mui/highlighters/search_highlighter.rb +2 -1
  24. data/lib/mui/highlighters/syntax_highlighter.rb +4 -1
  25. data/lib/mui/insert_completion_state.rb +15 -2
  26. data/lib/mui/key_handler/base.rb +87 -0
  27. data/lib/mui/key_handler/command_mode.rb +68 -0
  28. data/lib/mui/key_handler/insert_mode.rb +10 -41
  29. data/lib/mui/key_handler/normal_mode.rb +24 -51
  30. data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
  31. data/lib/mui/key_handler/search_mode.rb +10 -7
  32. data/lib/mui/key_handler/visual_mode.rb +15 -10
  33. data/lib/mui/key_notation_parser.rb +159 -0
  34. data/lib/mui/key_sequence.rb +67 -0
  35. data/lib/mui/key_sequence_buffer.rb +85 -0
  36. data/lib/mui/key_sequence_handler.rb +163 -0
  37. data/lib/mui/key_sequence_matcher.rb +79 -0
  38. data/lib/mui/line_renderer.rb +52 -1
  39. data/lib/mui/mode_manager.rb +3 -2
  40. data/lib/mui/screen.rb +30 -6
  41. data/lib/mui/search_state.rb +74 -27
  42. data/lib/mui/syntax/language_detector.rb +33 -1
  43. data/lib/mui/syntax/lexers/c_lexer.rb +2 -0
  44. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  45. data/lib/mui/syntax/lexers/go_lexer.rb +207 -0
  46. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  47. data/lib/mui/syntax/lexers/javascript_lexer.rb +219 -0
  48. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  49. data/lib/mui/syntax/lexers/ruby_lexer.rb +3 -0
  50. data/lib/mui/syntax/lexers/rust_lexer.rb +150 -0
  51. data/lib/mui/syntax/lexers/typescript_lexer.rb +225 -0
  52. data/lib/mui/terminal_adapter/base.rb +21 -0
  53. data/lib/mui/terminal_adapter/curses.rb +37 -11
  54. data/lib/mui/themes/default.rb +263 -132
  55. data/lib/mui/version.rb +1 -1
  56. data/lib/mui/window.rb +105 -39
  57. data/lib/mui/window_manager.rb +7 -0
  58. data/lib/mui/wrap_cache.rb +40 -0
  59. data/lib/mui/wrap_helper.rb +84 -0
  60. data/lib/mui.rb +15 -0
  61. metadata +26 -3
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Mui
6
+ # Parser for Vim-style key notation strings
7
+ # Converts notation like "<Leader>gd", "<C-x><C-s>", "<Space>w" to internal key arrays
8
+ module KeyNotationParser
9
+ # Special key mappings (case-insensitive)
10
+ SPECIAL_KEYS = {
11
+ "space" => " ",
12
+ "tab" => "\t",
13
+ "s-tab" => :shift_tab,
14
+ "btab" => :shift_tab,
15
+ "cr" => "\r",
16
+ "enter" => "\r",
17
+ "return" => "\r",
18
+ "esc" => "\e",
19
+ "escape" => "\e",
20
+ "bs" => "\x7f",
21
+ "backspace" => "\x7f",
22
+ "del" => "\x7f",
23
+ "delete" => "\x7f",
24
+ "lt" => "<",
25
+ "gt" => ">",
26
+ "bar" => "|",
27
+ "bslash" => "\\",
28
+ "leader" => :leader
29
+ }.freeze
30
+
31
+ # Ctrl key mappings (a-z and some special characters)
32
+ CTRL_CHARS = {
33
+ "@" => 0, # NUL
34
+ "a" => 1,
35
+ "b" => 2,
36
+ "c" => 3,
37
+ "d" => 4,
38
+ "e" => 5,
39
+ "f" => 6,
40
+ "g" => 7,
41
+ "h" => 8, # Also backspace
42
+ "i" => 9, # Also tab
43
+ "j" => 10, # Also newline
44
+ "k" => 11,
45
+ "l" => 12,
46
+ "m" => 13, # Also carriage return
47
+ "n" => 14,
48
+ "o" => 15,
49
+ "p" => 16,
50
+ "q" => 17,
51
+ "r" => 18,
52
+ "s" => 19,
53
+ "t" => 20,
54
+ "u" => 21,
55
+ "v" => 22,
56
+ "w" => 23,
57
+ "x" => 24,
58
+ "y" => 25,
59
+ "z" => 26,
60
+ "[" => 27, # Also escape
61
+ "\\" => 28,
62
+ "]" => 29,
63
+ "^" => 30,
64
+ "_" => 31
65
+ }.freeze
66
+
67
+ class << self
68
+ # Parse a key notation string into an array of keys
69
+ # @param notation [String] Key notation (e.g., "<Leader>gd", "<C-x><C-s>")
70
+ # @return [Array<String, Symbol>] Array of normalized keys
71
+ def parse(notation)
72
+ return [] if notation.nil? || notation.empty?
73
+
74
+ tokens = []
75
+ scanner = StringScanner.new(notation)
76
+
77
+ until scanner.eos?
78
+ if scanner.scan(/<([^>]+)>/)
79
+ # Special key notation <...>
80
+ tokens << parse_special(scanner[1])
81
+ else
82
+ # Regular character
83
+ char = scanner.getch
84
+ tokens << char if char
85
+ end
86
+ end
87
+
88
+ tokens
89
+ end
90
+
91
+ # Parse a special key notation (content inside < >)
92
+ # @param name [String] Special key name (e.g., "C-x", "Leader", "Space")
93
+ # @return [String, Symbol] Normalized key
94
+ def parse_special(name)
95
+ return :leader if name.casecmp?("leader")
96
+
97
+ # Check SPECIAL_KEYS first (handles <S-Tab>, <btab>, etc.)
98
+ normalized_name = name.downcase
99
+ return SPECIAL_KEYS[normalized_name] if SPECIAL_KEYS.key?(normalized_name)
100
+
101
+ # Handle Ctrl key: <C-x>, <Ctrl-x>, <C-X>
102
+ return parse_ctrl_key(::Regexp.last_match(2)) if name =~ /\A(c|ctrl)-(.+)\z/i
103
+
104
+ # Handle Shift key: <S-x>, <Shift-x>
105
+ return parse_shift_key(::Regexp.last_match(2)) if name =~ /\A(s|shift)-(.+)\z/i
106
+
107
+ # Unknown special key - return as-is
108
+ name
109
+ end
110
+
111
+ # Normalize an input key (from terminal) to internal representation
112
+ # @param key [Integer, String] Raw key input
113
+ # @return [String, nil] Normalized key string, or nil if invalid
114
+ def normalize_input_key(key)
115
+ case key
116
+ when String
117
+ key
118
+ when Integer
119
+ normalize_integer_key(key)
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def parse_ctrl_key(char)
126
+ char_lower = char.downcase
127
+ code = CTRL_CHARS[char_lower]
128
+ code ? code.chr : char
129
+ end
130
+
131
+ def parse_shift_key(char)
132
+ # Shift typically produces uppercase for letters
133
+ char.length == 1 ? char.upcase : char
134
+ end
135
+
136
+ def normalize_integer_key(key)
137
+ case key
138
+ when KeyCode::ENTER_CR, KeyCode::ENTER_LF
139
+ "\r"
140
+ when KeyCode::ESCAPE
141
+ "\e"
142
+ when KeyCode::TAB
143
+ "\t"
144
+ when 353 # Curses::KEY_BTAB (Shift+Tab)
145
+ :shift_tab
146
+ when KeyCode::BACKSPACE
147
+ "\x7f"
148
+ when 0..31
149
+ # Control characters - convert to the character they represent
150
+ key.chr
151
+ when KeyCode::PRINTABLE_MIN..KeyCode::PRINTABLE_MAX
152
+ key.chr(Encoding::UTF_8)
153
+ end
154
+ rescue RangeError
155
+ nil
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Represents a sequence of keys for key mapping
5
+ # Parses Vim-style notation and provides normalization for matching
6
+ class KeySequence
7
+ attr_reader :keys, :notation
8
+
9
+ # @param notation [String] Key notation string (e.g., "<Leader>gd", "<C-x><C-s>")
10
+ def initialize(notation)
11
+ @notation = notation
12
+ @keys = KeyNotationParser.parse(notation)
13
+ end
14
+
15
+ # Normalize the key sequence by expanding :leader to actual leader key
16
+ # @param leader_key [String] The actual leader key (e.g., "\\", " ")
17
+ # @return [Array<String>] Array of normalized key strings
18
+ def normalize(leader_key)
19
+ @keys.map { |k| k == :leader ? leader_key : k }
20
+ end
21
+
22
+ # Get the length of the key sequence
23
+ # @return [Integer] Number of keys in the sequence
24
+ def length
25
+ @keys.length
26
+ end
27
+
28
+ # Check if this sequence contains a leader key
29
+ # @return [Boolean]
30
+ def leader?
31
+ @keys.include?(:leader)
32
+ end
33
+
34
+ # Check if this is a single key sequence
35
+ # @return [Boolean]
36
+ def single_key?
37
+ @keys.length == 1
38
+ end
39
+
40
+ # Check equality with another KeySequence
41
+ # @param other [KeySequence]
42
+ # @return [Boolean]
43
+ def ==(other)
44
+ return false unless other.is_a?(KeySequence)
45
+
46
+ @keys == other.keys
47
+ end
48
+
49
+ alias eql? ==
50
+
51
+ # Hash for use in Hash keys
52
+ def hash
53
+ @keys.hash
54
+ end
55
+
56
+ # Convert back to notation string for display
57
+ # @return [String]
58
+ def to_s
59
+ @notation
60
+ end
61
+
62
+ # Inspect for debugging
63
+ def inspect
64
+ "#<Mui::KeySequence #{@notation.inspect} => #{@keys.inspect}>"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Buffer for accumulating key inputs for multi-key sequence matching
5
+ # Tracks timing for timeout detection
6
+ class KeySequenceBuffer
7
+ attr_reader :last_input_time
8
+
9
+ def initialize
10
+ @buffer = []
11
+ @last_input_time = nil
12
+ end
13
+
14
+ # Push a key into the buffer
15
+ # @param key [Integer, String] Raw key input from terminal
16
+ # @return [Boolean] true if key was added, false if key was invalid
17
+ def push(key)
18
+ normalized = KeyNotationParser.normalize_input_key(key)
19
+ return false unless normalized
20
+
21
+ @buffer << normalized
22
+ @last_input_time = Time.now
23
+ true
24
+ end
25
+
26
+ # Clear the buffer
27
+ def clear
28
+ @buffer.clear
29
+ @last_input_time = nil
30
+ end
31
+
32
+ # Check if buffer is empty
33
+ # @return [Boolean]
34
+ def empty?
35
+ @buffer.empty?
36
+ end
37
+
38
+ # Get buffer length
39
+ # @return [Integer]
40
+ def length
41
+ @buffer.length
42
+ end
43
+
44
+ alias size length
45
+
46
+ # Get copy of buffer contents as array
47
+ # @return [Array<String>]
48
+ def to_a
49
+ @buffer.dup
50
+ end
51
+
52
+ # Get the first key in the buffer
53
+ # @return [String, nil]
54
+ def first
55
+ @buffer.first
56
+ end
57
+
58
+ # Remove and return the first key
59
+ # @return [String, nil]
60
+ def shift
61
+ key = @buffer.shift
62
+ @last_input_time = nil if @buffer.empty?
63
+ key
64
+ end
65
+
66
+ # Check if the buffer has timed out
67
+ # @param timeout_ms [Integer] Timeout in milliseconds
68
+ # @return [Boolean]
69
+ def timeout?(timeout_ms)
70
+ return false unless @last_input_time
71
+ return false if @buffer.empty?
72
+
73
+ elapsed_ms = (Time.now - @last_input_time) * 1000
74
+ elapsed_ms > timeout_ms
75
+ end
76
+
77
+ # Get elapsed time since last input in milliseconds
78
+ # @return [Float, nil] Elapsed time in ms, or nil if no input
79
+ def elapsed_ms
80
+ return nil unless @last_input_time
81
+
82
+ (Time.now - @last_input_time) * 1000
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Main handler for multi-key sequence processing
5
+ # Integrates buffer, matcher, and timeout handling
6
+ class KeySequenceHandler
7
+ DEFAULT_TIMEOUT_MS = 1000
8
+
9
+ # Process result types
10
+ RESULT_HANDLED = :handled # Handler executed
11
+ RESULT_PENDING = :pending # Waiting for more keys
12
+ RESULT_PASSTHROUGH = :passthrough # No match, pass key to built-in handler
13
+
14
+ attr_reader :buffer
15
+
16
+ # @param config [Config] Configuration object
17
+ def initialize(config)
18
+ @config = config
19
+ @buffer = KeySequenceBuffer.new
20
+ @keymaps = {} # { mode => { KeySequence => handler } }
21
+ @pending_handler = nil # Handler for exact match while waiting for longer
22
+ end
23
+
24
+ # Get the leader key from config
25
+ # @return [String]
26
+ def leader_key
27
+ @config.get(:leader) || "\\"
28
+ end
29
+
30
+ # Get the timeout in milliseconds
31
+ # @return [Integer]
32
+ def timeout_ms
33
+ @config.get(:timeoutlen) || DEFAULT_TIMEOUT_MS
34
+ end
35
+
36
+ # Register a key sequence mapping
37
+ # @param mode [Symbol] Mode (:normal, :insert, etc.)
38
+ # @param key_notation [String] Key notation (e.g., "<Leader>gd")
39
+ # @param handler [Proc] Handler to execute
40
+ def register(mode, key_notation, handler)
41
+ @keymaps[mode] ||= {}
42
+ sequence = KeySequence.new(key_notation)
43
+ @keymaps[mode][sequence] = handler
44
+ end
45
+
46
+ # Process an input key
47
+ # @param key [Integer, String] Raw key input
48
+ # @param mode [Symbol] Current mode
49
+ # @return [Array<Symbol, Object>] [result_type, data]
50
+ # - [:handled, handler] - Execute the handler
51
+ # - [:pending, nil] - Wait for more input
52
+ # - [:passthrough, key] - Pass key to built-in handler
53
+ def process(key, mode)
54
+ # Check timeout first - if timed out, handle before processing new key
55
+ if @buffer.timeout?(timeout_ms) && !@buffer.empty?
56
+ result = handle_timeout(mode)
57
+ # If we got a result, return it; the new key will be processed next time
58
+ return result if result[0] == RESULT_HANDLED
59
+
60
+ # If passthrough, clear buffer and continue with new key
61
+ @buffer.clear
62
+ @pending_handler = nil
63
+ end
64
+
65
+ # Add key to buffer
66
+ unless @buffer.push(key)
67
+ # Invalid key, pass through as-is
68
+ return [RESULT_PASSTHROUGH, key]
69
+ end
70
+
71
+ # Match against keymaps
72
+ matcher = KeySequenceMatcher.new(@keymaps, leader_key)
73
+ match_type, handler = matcher.match(mode, @buffer.to_a)
74
+
75
+ case match_type
76
+ when KeySequenceMatcher::MATCH_EXACT
77
+ has_longer = matcher.longer_sequences?(mode, @buffer.to_a)
78
+ if has_longer
79
+ # Exact match but longer sequences exist
80
+ # Store handler and wait for more input or timeout
81
+ @pending_handler = handler
82
+ [RESULT_PENDING, nil]
83
+ else
84
+ # Exact match, no longer sequences - execute immediately
85
+ @buffer.clear
86
+ @pending_handler = nil
87
+ [RESULT_HANDLED, handler]
88
+ end
89
+
90
+ when KeySequenceMatcher::MATCH_PARTIAL
91
+ # Could become a match, wait for more input
92
+ [RESULT_PENDING, nil]
93
+
94
+ else
95
+ # No match - pass through first key
96
+ handle_no_match
97
+ end
98
+ end
99
+
100
+ # Check for timeout and handle if needed (called from main loop)
101
+ # @param mode [Symbol] Current mode
102
+ # @return [Array<Symbol, Object>, nil] Result if timed out, nil otherwise
103
+ def check_timeout(mode)
104
+ return nil if @buffer.empty?
105
+ return nil unless @buffer.timeout?(timeout_ms)
106
+
107
+ handle_timeout(mode)
108
+ end
109
+
110
+ # Check if there are pending keys in the buffer
111
+ # @return [Boolean]
112
+ def pending?
113
+ !@buffer.empty?
114
+ end
115
+
116
+ # Clear the buffer and pending state
117
+ def clear
118
+ @buffer.clear
119
+ @pending_handler = nil
120
+ end
121
+
122
+ # Get pending key display for status line
123
+ # @return [String, nil]
124
+ def pending_keys_display
125
+ return nil if @buffer.empty?
126
+
127
+ @buffer.to_a.join
128
+ end
129
+
130
+ # Rebuild internal keymaps from config
131
+ # Called after config changes
132
+ def rebuild_keymaps
133
+ @keymaps = {}
134
+ @config.keymaps.each do |mode, mappings|
135
+ mappings.each do |key_notation, handler|
136
+ register(mode, key_notation, handler)
137
+ end
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def handle_timeout(_mode)
144
+ if @pending_handler
145
+ # We had an exact match, execute it
146
+ handler = @pending_handler
147
+ @buffer.clear
148
+ @pending_handler = nil
149
+ [RESULT_HANDLED, handler]
150
+ else
151
+ # No exact match, passthrough first key
152
+ handle_no_match
153
+ end
154
+ end
155
+
156
+ def handle_no_match
157
+ first_key = @buffer.shift
158
+ @buffer.clear
159
+ @pending_handler = nil
160
+ [RESULT_PASSTHROUGH, first_key]
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Matches input key sequences against registered key mappings
5
+ class KeySequenceMatcher
6
+ # Match result types
7
+ MATCH_EXACT = :exact # Complete match found
8
+ MATCH_PARTIAL = :partial # Input is prefix of one or more registered sequences
9
+ MATCH_NONE = :none # No match possible
10
+
11
+ # @param keymaps [Hash] Mode => { KeySequence => handler }
12
+ # @param leader_key [String] The leader key to expand :leader symbols
13
+ def initialize(keymaps, leader_key)
14
+ @keymaps = keymaps
15
+ @leader_key = leader_key
16
+ end
17
+
18
+ # Match input keys against registered keymaps for a mode
19
+ # @param mode [Symbol] The current mode (:normal, :insert, etc.)
20
+ # @param input_keys [Array<String>] Array of normalized input keys
21
+ # @return [Array<Symbol, Object>] [match_type, handler_or_nil]
22
+ def match(mode, input_keys)
23
+ mode_keymaps = @keymaps[mode]
24
+ return [MATCH_NONE, nil] unless mode_keymaps
25
+ return [MATCH_NONE, nil] if input_keys.empty?
26
+
27
+ exact_match = nil
28
+ has_longer_match = false
29
+
30
+ mode_keymaps.each do |sequence, handler|
31
+ seq_keys = sequence.normalize(@leader_key)
32
+
33
+ if seq_keys == input_keys
34
+ # Exact match found
35
+ exact_match = handler
36
+ elsif prefix_match?(input_keys, seq_keys)
37
+ # Input is a prefix of this sequence (longer sequence exists)
38
+ has_longer_match = true
39
+ end
40
+ end
41
+
42
+ if exact_match
43
+ # Exact match found - return it
44
+ # If there are also longer matches, caller may want to wait for timeout
45
+ [MATCH_EXACT, exact_match]
46
+ elsif has_longer_match
47
+ # No exact match, but input could lead to a match
48
+ [MATCH_PARTIAL, nil]
49
+ else
50
+ # No match possible
51
+ [MATCH_NONE, nil]
52
+ end
53
+ end
54
+
55
+ # Check if there are any longer sequences that could match
56
+ # Used to determine if we should wait for more input
57
+ # @param mode [Symbol]
58
+ # @param input_keys [Array<String>]
59
+ # @return [Boolean]
60
+ def longer_sequences?(mode, input_keys)
61
+ mode_keymaps = @keymaps[mode]
62
+ return false unless mode_keymaps
63
+
64
+ mode_keymaps.any? do |sequence, _handler|
65
+ seq_keys = sequence.normalize(@leader_key)
66
+ seq_keys.length > input_keys.length && prefix_match?(input_keys, seq_keys)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Check if input_keys is a prefix of seq_keys
73
+ def prefix_match?(input_keys, seq_keys)
74
+ return false if input_keys.length >= seq_keys.length
75
+
76
+ seq_keys.take(input_keys.length) == input_keys
77
+ end
78
+ end
79
+ end
@@ -17,8 +17,59 @@ module Mui
17
17
  render_with_highlights(screen, line, x, y, highlights)
18
18
  end
19
19
 
20
+ # Renders a wrapped line segment with screen coordinate-based highlights
21
+ # wrap_info: { text:, start_col:, end_col: }
22
+ # options: { selection:, search_state:, logical_row:, visible_width: }
23
+ def render_wrapped_line(screen, y, x, wrap_info, options = {})
24
+ text = wrap_info[:text]
25
+ return if text.nil?
26
+
27
+ logical_row = options[:logical_row]
28
+ start_col = wrap_info[:start_col]
29
+ end_col = wrap_info[:end_col]
30
+
31
+ # Collect highlights for this row and clip to wrapped segment range
32
+ highlights = collect_highlights(logical_row, text, options)
33
+ clipped_highlights = clip_highlights_to_range(highlights, start_col, end_col)
34
+
35
+ # Adjust highlight positions to be relative to wrap segment start
36
+ adjusted_highlights = clipped_highlights.map do |h|
37
+ adjusted_start = h.start_col - start_col
38
+ adjusted_end = h.end_col - start_col
39
+ Highlight.new(
40
+ start_col: adjusted_start,
41
+ end_col: adjusted_end,
42
+ style: h.style,
43
+ priority: h.priority
44
+ )
45
+ end
46
+
47
+ render_with_highlights(screen, text, x, y, adjusted_highlights)
48
+ end
49
+
20
50
  private
21
51
 
52
+ # Clips highlights to a column range and returns only overlapping portions
53
+ def clip_highlights_to_range(highlights, range_start, range_end)
54
+ highlights.filter_map do |h|
55
+ # Skip if highlight doesn't overlap with range
56
+ next if h.end_col < range_start || h.start_col >= range_end
57
+
58
+ # Clip to range
59
+ clipped_start = [h.start_col, range_start].max
60
+ clipped_end = [h.end_col, range_end - 1].min
61
+
62
+ next if clipped_start > clipped_end
63
+
64
+ Highlight.new(
65
+ start_col: clipped_start,
66
+ end_col: clipped_end,
67
+ style: h.style,
68
+ priority: h.priority
69
+ )
70
+ end
71
+ end
72
+
22
73
  def collect_highlights(row, line, options)
23
74
  @highlighters
24
75
  .flat_map { |h| h.highlights_for(row, line, options) }
@@ -36,7 +87,7 @@ module Mui
36
87
 
37
88
  segments.each do |segment|
38
89
  put_text(screen, y, current_x, segment[:text], segment[:style])
39
- current_x += segment[:text].length
90
+ current_x += UnicodeWidth.string_width(segment[:text])
40
91
  end
41
92
  end
42
93
 
@@ -4,9 +4,9 @@ module Mui
4
4
  # Manages editor mode state and transitions
5
5
  class ModeManager
6
6
  attr_reader :mode, :selection, :register, :undo_manager, :search_state, :search_input, :editor,
7
- :last_visual_selection
7
+ :last_visual_selection, :key_sequence_handler
8
8
 
9
- def initialize(window:, buffer:, command_line:, undo_manager: nil, editor: nil, register: nil)
9
+ def initialize(window:, buffer:, command_line:, undo_manager: nil, editor: nil, register: nil, key_sequence_handler: nil)
10
10
  @tab_manager = window.is_a?(TabManager) ? window : nil
11
11
  @window_manager = window.is_a?(WindowManager) ? window : nil
12
12
  @window = !@tab_manager && !@window_manager ? window : nil
@@ -15,6 +15,7 @@ module Mui
15
15
  @register = register || Mui.register
16
16
  @undo_manager = undo_manager
17
17
  @editor = editor
18
+ @key_sequence_handler = key_sequence_handler
18
19
  @search_state = SearchState.new
19
20
  @search_input = SearchInput.new
20
21
  @mode = Mode::NORMAL