mui 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f38d3d434535fc336c9b5394801e3fb49393f691b15878a1994758cfb73a6217
4
- data.tar.gz: 220128d17afb0f711f2642cd7e64acc14364ca5e65de77c7130828e5a809796c
3
+ metadata.gz: fc6a6725223b44bd937314729fc4008b726123dd1624adfdf3adc868b9d4d71d
4
+ data.tar.gz: 1c58af876fa5c06fb9033a9968dd1de012265f3d3b85bb50a46b17a9e24eeca5
5
5
  SHA512:
6
- metadata.gz: 82468df468d286b1b672b4c47257565f348661cf38633fdabfdc021cf42639d3637f1b6a1b233bd7af65b47fa426b15da2cd52e270cbe7be987dd071e4d785db
7
- data.tar.gz: dbf19ebadd262acc004320ebcf55d25f404ce22ad7a77beb08f4363d5154dc2f9241834f531fa15b4b953d8768bc3c1ec8213441d3150ec803081b1be32590b0
6
+ metadata.gz: a7a0d46bdf37d31dd6a475b4c42665462e15ed2eee1cad432eed7dcec13776974cd95ed1853e2856fffae2005d367ae6a78294d30e2343ee25240995e943c5dc
7
+ data.tar.gz: 1f7b17692e0f2349eadbdd38b3687ca661f6d8f6196ba162579e28cc8b7f837d00d36d1ba1533a57d52e3e20f0f1510f51dc5333c2b494bc053c6533e62fd324
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-12-14 12:18:50 UTC using RuboCop version 1.81.7.
3
+ # on 2025-12-18 10:56:40 UTC using RuboCop version 1.81.7.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -48,7 +48,7 @@ Lint/UselessConstantScoping:
48
48
  Exclude:
49
49
  - 'lib/mui/buffer_word_cache.rb'
50
50
 
51
- # Offense count: 33
51
+ # Offense count: 35
52
52
  # Configuration parameters: AllowedMethods, AllowedPatterns.
53
53
  Metrics/CyclomaticComplexity:
54
54
  Max: 49
@@ -56,14 +56,14 @@ Metrics/CyclomaticComplexity:
56
56
  # Offense count: 2
57
57
  # Configuration parameters: CountComments, CountAsOne.
58
58
  Metrics/ModuleLength:
59
- Max: 284
59
+ Max: 328
60
60
 
61
61
  # Offense count: 10
62
62
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
63
63
  Metrics/ParameterLists:
64
64
  Max: 9
65
65
 
66
- # Offense count: 14
66
+ # Offense count: 15
67
67
  # Configuration parameters: AllowedMethods, AllowedPatterns.
68
68
  Metrics/PerceivedComplexity:
69
69
  Max: 19
@@ -123,7 +123,7 @@ Naming/PredicateMethod:
123
123
  - 'lib/mui/undo_manager.rb'
124
124
  - 'lib/mui/window_manager.rb'
125
125
 
126
- # Offense count: 1
126
+ # Offense count: 4
127
127
  # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
128
128
  # NamePrefix: is_, has_, have_, does_
129
129
  # ForbiddenPrefixes: is_, has_, have_, does_
@@ -133,6 +133,9 @@ Naming/PredicatePrefix:
133
133
  Exclude:
134
134
  - 'spec/**/*'
135
135
  - 'lib/mui/search_state.rb'
136
+ - 'lib/mui/terminal_adapter/base.rb'
137
+ - 'lib/mui/terminal_adapter/curses.rb'
138
+ - 'test/test_helper.rb'
136
139
 
137
140
  # Offense count: 2
138
141
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] 2025-12-19
4
+
5
+ ### Added
6
+ - `<S-Tab>` (Shift+Tab) notation support in keymap configuration:
7
+ - Added `s-tab` and `btab` to `SPECIAL_KEYS` mapping to `:shift_tab` symbol
8
+ - `normalize_input_key()` now handles `KEY_BTAB` (353) from Curses
9
+ - Enables tab navigation keymaps like:
10
+ ```ruby
11
+ Mui.keymap :normal, "<Tab>", ->(ctx) { ctx.editor.tab_manager.next_tab }
12
+ Mui.keymap :normal, "<S-Tab>", ->(ctx) { ctx.editor.tab_manager.prev_tab }
13
+ ```
14
+ - Function definition name highlighting for 6 languages:
15
+ - Ruby: `def hello` highlights `hello` (lookbehind pattern `(?<=def )`)
16
+ - Go: `func main()` highlights `main` (lookbehind pattern `(?<=func )`)
17
+ - Rust: `fn calculate()` highlights `calculate` (lookbehind pattern `(?<=fn )`)
18
+ - JavaScript: `function add()` highlights `add` (post-process after `function` keyword)
19
+ - TypeScript: `function fetchData()` highlights `fetchData` (post-process after `function` keyword)
20
+ - C: `int main()` highlights `main` (lookahead pattern for identifier before `(`)
21
+ - New token type `:function_definition` mapped to `syntax_function_definition` color scheme element
22
+ - All 8 themes updated with `syntax_function_definition` style (same color as `syntax_method_call`)
23
+ - Improved mui default theme with 256-color palette:
24
+ - Eye-friendly gray-based color scheme using 256-color palette
25
+ - 19 new `mui_*` colors in `EXTENDED_COLOR_MAP` (mui_bg, mui_fg, mui_comment, etc.)
26
+ - Syntax highlighting follows Vim standard highlight groups (Comment, Constant, Identifier, Statement, PreProc, Type, Special)
27
+ - 8-color fallbacks for all mui colors
28
+ - Improved all bundled themes to match their original color schemes:
29
+ - Solarized Dark/Light: Adjusted visual selection, operators, and diff colors
30
+ - Monokai: Fixed comment color to olive gray (#75715e), added `monokai_comment` color
31
+ - Nord: Adjusted status line and constant colors to match Nord palette
32
+ - Gruvbox: Adjusted identifier and keyword colors
33
+ - Dracula: Adjusted visual selection and symbol colors
34
+ - Tokyo Night: Adjusted operator and constant colors
35
+ - Added LSP diagnostics and floating window styles to all themes:
36
+ - `diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, `diagnostic_hint`
37
+ - `floating_window`
38
+ - 256-color support stabilization:
39
+ - Environment capability detection (`Curses.has_colors?`, `Curses.colors`, `Curses.color_pairs`)
40
+ - Automatic 8-color fallback for terminals without 256-color support
41
+ - `FALLBACK_MAP` to map extended colors to nearest basic 8-color equivalents
42
+ - LRU cache for color pairs to prevent pair exhaustion
43
+ - Automatic eviction of oldest pairs when limit is reached
44
+ - Backward compatible: `ColorManager.new` without adapter assumes 256 colors
45
+ - `ColorManager#supports_256_colors` to check color capability
46
+ - `TerminalAdapter::Base#has_colors?`, `#colors`, `#color_pairs` interface methods
47
+
48
+ ### Fixed
49
+ - Fixed text corruption after closing floating window or completion popup:
50
+ - Japanese/CJK multibyte characters were not displaying correctly after popup closed
51
+ - Added `touchwin` method to force complete screen redraw using `Curses.stdscr.redraw`
52
+ - `FloatingWindow` now tracks `last_bounds` and `needs_clear` flag
53
+ - `InsertCompletionState` now tracks `needs_clear` flag
54
+ - Editor calls `touchwin` when popup is closed to restore corrupted characters
55
+
56
+ ### Changed
57
+ - Improved search performance for large files:
58
+ - Added row-based index (`row_index`) to search match cache for O(1) lookup
59
+ - `matches_for_row()` now uses hash lookup instead of linear search
60
+ - Before: O(M × visible_rows) per frame, After: O(visible_rows) per frame
61
+ - Improved navigation performance for large files (G, gg commands):
62
+ - Added smart scroll jump for cursor movements over 100 rows
63
+ - Directly calculates scroll position instead of iterating line by line
64
+ - Before: O(n) where n = cursor distance, After: O(1)
65
+
3
66
  ## [0.3.0] - 2025-12-15
4
67
 
5
68
  ### Added
@@ -36,15 +36,27 @@ Configuration files are written in Ruby using Mui's DSL.
36
36
  Mui.set :colorscheme, "tokyo_night"
37
37
  ```
38
38
 
39
- Available themes:
40
- - `mui` (default)
41
- - `solarized_dark`
42
- - `solarized_light`
43
- - `monokai`
44
- - `nord`
45
- - `gruvbox_dark`
46
- - `dracula`
47
- - `tokyo_night`
39
+ Available themes (all themes support 256-color palette with 8-color fallback):
40
+
41
+ | Theme | Description |
42
+ |-------|-------------|
43
+ | `mui` | Default theme with eye-friendly gray-based colors |
44
+ | `solarized_dark` | Solarized dark theme |
45
+ | `solarized_light` | Solarized light theme |
46
+ | `monokai` | Monokai theme |
47
+ | `nord` | Nord theme |
48
+ | `gruvbox_dark` | Gruvbox dark theme |
49
+ | `dracula` | Dracula theme |
50
+ | `tokyo_night` | Tokyo Night theme |
51
+
52
+ ### 256-Color Support
53
+
54
+ All bundled themes utilize 256-color palettes for rich syntax highlighting and UI elements. Mui automatically detects your terminal's color capabilities:
55
+
56
+ - **256-color terminals**: Full color palette with all theme colors
57
+ - **8-color terminals**: Automatic fallback to basic 8-color equivalents
58
+
59
+ Most modern terminals (iTerm2, gnome-terminal, Windows Terminal, etc.) support 256 colors. If colors appear incorrect, ensure your terminal's `TERM` environment variable is set correctly (e.g., `xterm-256color`).
48
60
 
49
61
  ### Indentation
50
62
 
@@ -125,6 +137,7 @@ end
125
137
  | `<Leader>` | Leader key |
126
138
  | `<Space>` | Space bar |
127
139
  | `<Tab>` | Tab key |
140
+ | `<S-Tab>`, `<btab>` | Shift+Tab |
128
141
  | `<CR>`, `<Enter>` | Enter key |
129
142
  | `<Esc>` | Escape key |
130
143
  | `<BS>` | Backspace |
@@ -144,6 +144,12 @@ Each theme defines colors for:
144
144
  | `syntax_number` | Numeric literals |
145
145
  | `syntax_type` | Types and classes |
146
146
  | `syntax_function` | Function names |
147
+ | `syntax_function_definition` | Function definition names |
147
148
  | `syntax_variable` | Variables |
148
149
  | `syntax_constant` | Constants |
149
150
  | `syntax_operator` | Operators |
151
+ | `diagnostic_error` | LSP diagnostic errors |
152
+ | `diagnostic_warning` | LSP diagnostic warnings |
153
+ | `diagnostic_info` | LSP diagnostic information |
154
+ | `diagnostic_hint` | LSP diagnostic hints |
155
+ | `floating_window` | Floating window background |
@@ -17,8 +17,27 @@ module Mui
17
17
  # 256-color palette extended colors
18
18
  # Use https://www.ditig.com/256-colors-cheat-sheet for reference
19
19
  EXTENDED_COLOR_MAP = {
20
- # mui theme
21
- darkgray: 235, # #262626 (~#2b2b2b)
20
+ # mui theme - Eye-friendly gray-based theme
21
+ mui_bg: 236, # #303030 - Calm dark gray background
22
+ mui_fg: 253, # #dadada - Soft white (easy on the eyes)
23
+ mui_comment: 102, # #878787 - Subtle gray (for comments)
24
+ mui_constant: 110, # #87afd7 - Calm blue (constants/strings/numbers)
25
+ mui_identifier: 174, # #d78787 - Soft salmon pink
26
+ mui_statement: 186, # #d7d787 - Subtle yellow (keywords)
27
+ mui_preproc: 173, # #d7875f - Orange/brown (preprocessor)
28
+ mui_type: 109, # #87afaf - Calm cyan (types)
29
+ mui_special: 180, # #d7af87 - Soft beige (symbols)
30
+ mui_function: 216, # #ffaf87 - Peach/orange (functions)
31
+ # UI colors
32
+ mui_line_number: 243, # #767676 - Subtle gray
33
+ mui_status_bg: 238, # #444444 - Status bar background
34
+ mui_visual: 239, # #4e4e4e - Selection background
35
+ mui_search: 222, # #ffd787 - Search highlight (prominent yellow)
36
+ mui_tab_bg: 237, # #3a3a3a - Tab bar background
37
+ mui_tab_active: 110, # #87afd7 - Active tab
38
+ mui_error: 167, # #d75f5f - Error messages
39
+ mui_info: 109, # #87afaf - Info messages
40
+ darkgray: 235, # #262626 (~#2b2b2b) - Kept for backward compatibility
22
41
 
23
42
  # solarized
24
43
  solarized_base03: 234, # #1c1c1c (~#002b36)
@@ -41,6 +60,7 @@ module Mui
41
60
  # monokai
42
61
  monokai_bg: 235, # #262626 (~#272822)
43
62
  monokai_fg: 231, # #ffffff (~#f8f8f2)
63
+ monokai_comment: 101, # #87875f (~#75715e) - Olive gray for comments
44
64
  monokai_pink: 197, # #ff005f (~#f92672)
45
65
  monokai_green: 148, # #afd700 (~#a6e22e)
46
66
  monokai_orange: 208, # #ff8700 (~#fd971f)
@@ -104,18 +124,95 @@ module Mui
104
124
  tokyo_yellow: 223 # #ffd7af (~#e0af68)
105
125
  }.freeze
106
126
 
107
- attr_reader :pairs
127
+ # Fallback map: 256-color to 8-color
128
+ FALLBACK_MAP = {
129
+ # mui theme
130
+ mui_bg: :black,
131
+ mui_fg: :white,
132
+ mui_comment: :white,
133
+ mui_constant: :cyan,
134
+ mui_identifier: :red,
135
+ mui_statement: :yellow,
136
+ mui_preproc: :yellow,
137
+ mui_type: :cyan,
138
+ mui_special: :yellow,
139
+ mui_function: :yellow,
140
+ mui_line_number: :white,
141
+ mui_status_bg: :blue,
142
+ mui_visual: :magenta,
143
+ mui_search: :yellow,
144
+ mui_tab_bg: :blue,
145
+ mui_tab_active: :cyan,
146
+ mui_error: :red,
147
+ mui_info: :cyan,
148
+ darkgray: :black,
149
+ # solarized
150
+ solarized_base03: :black, solarized_base02: :black,
151
+ solarized_base01: :white, solarized_base00: :white,
152
+ solarized_base0: :white, solarized_base1: :white,
153
+ solarized_base2: :white, solarized_base3: :white,
154
+ solarized_yellow: :yellow, solarized_orange: :red,
155
+ solarized_red: :red, solarized_magenta: :magenta,
156
+ solarized_violet: :blue, solarized_blue: :blue,
157
+ solarized_cyan: :cyan, solarized_green: :green,
158
+ # monokai
159
+ monokai_bg: :black, monokai_fg: :white, monokai_comment: :white,
160
+ monokai_pink: :magenta, monokai_green: :green,
161
+ monokai_orange: :yellow, monokai_purple: :magenta,
162
+ monokai_cyan: :cyan, monokai_yellow: :yellow,
163
+ # nord
164
+ nord_polar0: :black, nord_polar1: :black,
165
+ nord_polar2: :black, nord_polar3: :white,
166
+ nord_snow0: :white, nord_snow1: :white, nord_snow2: :white,
167
+ nord_frost0: :cyan, nord_frost1: :cyan,
168
+ nord_frost2: :blue, nord_frost3: :blue,
169
+ nord_aurora_red: :red, nord_aurora_orange: :yellow,
170
+ nord_aurora_yellow: :yellow, nord_aurora_green: :green,
171
+ nord_aurora_purple: :magenta,
172
+ # gruvbox
173
+ gruvbox_bg: :black, gruvbox_fg: :white,
174
+ gruvbox_red: :red, gruvbox_green: :green,
175
+ gruvbox_yellow: :yellow, gruvbox_blue: :blue,
176
+ gruvbox_purple: :magenta, gruvbox_aqua: :cyan,
177
+ gruvbox_orange: :yellow, gruvbox_gray: :white,
178
+ # dracula
179
+ dracula_bg: :black, dracula_fg: :white,
180
+ dracula_selection: :black, dracula_comment: :blue,
181
+ dracula_cyan: :cyan, dracula_green: :green,
182
+ dracula_orange: :yellow, dracula_pink: :magenta,
183
+ dracula_purple: :magenta, dracula_red: :red,
184
+ dracula_yellow: :yellow,
185
+ # tokyo night
186
+ tokyo_bg: :black, tokyo_fg: :white,
187
+ tokyo_comment: :blue, tokyo_cyan: :cyan,
188
+ tokyo_blue: :blue, tokyo_purple: :magenta,
189
+ tokyo_green: :green, tokyo_orange: :yellow,
190
+ tokyo_red: :red, tokyo_yellow: :yellow
191
+ }.freeze
108
192
 
109
- def initialize
193
+ attr_reader :pairs, :supports_256_colors
194
+
195
+ def initialize(adapter: nil)
110
196
  @pair_index = 1
111
197
  @pairs = {}
198
+ @pair_order = []
199
+ @adapter = adapter
200
+ configure_color_capability
112
201
  end
113
202
 
114
203
  def register_pair(fg, bg)
115
204
  key = [fg, bg]
116
- return @pairs[key] if @pairs[key]
205
+
206
+ if @pairs[key]
207
+ touch_pair(key)
208
+ return @pairs[key]
209
+ end
210
+
211
+ # Check pair limit and evict oldest if needed
212
+ evict_oldest_pair if @max_pairs.positive? && @pair_index >= @max_pairs
117
213
 
118
214
  @pairs[key] = @pair_index
215
+ @pair_order << key
119
216
  @pair_index += 1
120
217
  @pairs[key]
121
218
  end
@@ -128,9 +225,46 @@ module Mui
128
225
  return -1 if color.nil?
129
226
  return color if color.is_a?(Integer)
130
227
 
131
- COLOR_MAP[color] || EXTENDED_COLOR_MAP[color] || -1
228
+ resolved_color = resolve_with_fallback(color)
229
+ COLOR_MAP[resolved_color] || EXTENDED_COLOR_MAP[resolved_color] || -1
132
230
  end
133
231
 
134
232
  alias resolve color_code
233
+
234
+ private
235
+
236
+ def configure_color_capability
237
+ if @adapter.nil?
238
+ # Backward compatibility: assume 256 colors when adapter is not specified
239
+ @available_colors = 256
240
+ @max_pairs = 256
241
+ @supports_256_colors = true
242
+ elsif @adapter.has_colors?
243
+ @available_colors = @adapter.colors
244
+ @max_pairs = [@adapter.color_pairs, 256].min
245
+ @supports_256_colors = @available_colors >= 256
246
+ else
247
+ @available_colors = 0
248
+ @max_pairs = 0
249
+ @supports_256_colors = false
250
+ end
251
+ end
252
+
253
+ def resolve_with_fallback(color)
254
+ return color if @supports_256_colors
255
+ return color if COLOR_MAP.key?(color)
256
+
257
+ FALLBACK_MAP[color] || :white
258
+ end
259
+
260
+ def touch_pair(key)
261
+ @pair_order.delete(key)
262
+ @pair_order << key
263
+ end
264
+
265
+ def evict_oldest_pair
266
+ oldest_key = @pair_order.shift
267
+ @pairs.delete(oldest_key) if oldest_key
268
+ end
135
269
  end
136
270
  end
@@ -27,6 +27,7 @@ module Mui
27
27
  syntax_instance_variable
28
28
  syntax_global_variable
29
29
  syntax_method_call
30
+ syntax_function_definition
30
31
  syntax_type
31
32
  diff_add
32
33
  diff_delete
data/lib/mui/editor.rb CHANGED
@@ -238,8 +238,20 @@ module Mui
238
238
  end
239
239
 
240
240
  def render
241
+ # Force complete redraw if floating window or completion popup was closed
242
+ # This is needed because multibyte characters (CJK) can be corrupted
243
+ # when partially overwritten by popups
244
+ if @floating_window.needs_clear? || @insert_completion_state.needs_clear?
245
+ @screen.touchwin
246
+ @insert_completion_state.clear_needs_clear
247
+ end
248
+
241
249
  @screen.clear
242
250
 
251
+ # Clear the area where the floating window was previously displayed
252
+ # Must be done before window rendering to avoid overwriting text
253
+ @floating_window.clear_last_bounds(@screen) if @floating_window.needs_clear?
254
+
243
255
  @tab_bar_renderer.render(@screen, 0)
244
256
 
245
257
  window.ensure_cursor_visible
@@ -3,7 +3,7 @@
3
3
  module Mui
4
4
  # A floating window (popup) for displaying temporary content like hover info
5
5
  class FloatingWindow
6
- attr_reader :content, :row, :col, :width, :height
6
+ attr_reader :content, :row, :col, :width, :height, :last_bounds
7
7
  attr_accessor :visible
8
8
 
9
9
  def initialize(color_scheme)
@@ -15,6 +15,8 @@ module Mui
15
15
  @height = 0
16
16
  @visible = false
17
17
  @scroll_offset = 0
18
+ @last_bounds = nil
19
+ @needs_clear = false
18
20
  end
19
21
 
20
22
  # Show the floating window with content at the specified position
@@ -27,14 +29,49 @@ module Mui
27
29
  @scroll_offset = 0
28
30
  calculate_dimensions
29
31
  @visible = true
32
+ @needs_clear = false
30
33
  end
31
34
 
32
35
  # Hide the floating window
33
36
  def hide
37
+ return unless @visible
38
+
39
+ # Record bounds for clearing on next render
40
+ @last_bounds = {
41
+ row: @row,
42
+ col: @col,
43
+ width: @width,
44
+ height: @height
45
+ }
46
+ @needs_clear = true
34
47
  @visible = false
35
48
  @content = []
36
49
  end
37
50
 
51
+ # Check if the previous window area needs to be cleared
52
+ def needs_clear?
53
+ @needs_clear && @last_bounds
54
+ end
55
+
56
+ # Clear the area where the floating window was previously displayed
57
+ def clear_last_bounds(screen)
58
+ return unless needs_clear?
59
+
60
+ bounds = @last_bounds
61
+ adjusted_row, adjusted_col = adjust_position_for_bounds(screen, bounds)
62
+
63
+ bounds[:height].times do |i|
64
+ row = adjusted_row + i
65
+ next if row.negative? || row >= screen.height
66
+
67
+ spaces = " " * bounds[:width]
68
+ screen.put(row, adjusted_col, spaces)
69
+ end
70
+
71
+ @needs_clear = false
72
+ @last_bounds = nil
73
+ end
74
+
38
75
  # Scroll content up
39
76
  def scroll_up
40
77
  @scroll_offset = [@scroll_offset - 1, 0].max if @visible
@@ -105,6 +142,21 @@ module Mui
105
142
  [row, col]
106
143
  end
107
144
 
145
+ def adjust_position_for_bounds(screen, bounds)
146
+ row = bounds[:row]
147
+ col = bounds[:col]
148
+
149
+ # Adjust horizontal position
150
+ col = screen.width - bounds[:width] if col + bounds[:width] > screen.width
151
+ col = [col, 0].max
152
+
153
+ # Adjust vertical position
154
+ row = bounds[:row] - bounds[:height] if row + bounds[:height] > screen.height
155
+ row = [row, 0].max
156
+
157
+ [row, col]
158
+ end
159
+
108
160
  def draw_border(screen, row, col)
109
161
  style = @color_scheme[:floating_window] || @color_scheme[:completion_popup]
110
162
 
@@ -19,6 +19,7 @@ module Mui
19
19
  instance_variable: :syntax_instance_variable,
20
20
  global_variable: :syntax_global_variable,
21
21
  method_call: :syntax_method_call,
22
+ function_definition: :syntax_function_definition,
22
23
  type: :syntax_type,
23
24
  macro: :syntax_keyword, # Rust macros (println!, vec!, etc.)
24
25
  regex: :syntax_string # JavaScript/TypeScript regex literals
@@ -6,16 +6,29 @@ module Mui
6
6
  attr_reader :items, :selected_index, :prefix, :original_items
7
7
 
8
8
  def initialize
9
- reset
9
+ @needs_clear = false
10
+ reset(set_needs_clear: false)
10
11
  end
11
12
 
12
- def reset
13
+ def reset(set_needs_clear: true)
14
+ # Set needs_clear flag if we had items (popup was visible)
15
+ @needs_clear = true if set_needs_clear && !@items.empty?
13
16
  @items = []
14
17
  @original_items = []
15
18
  @selected_index = 0
16
19
  @prefix = ""
17
20
  end
18
21
 
22
+ # Check if the previous popup area needs to be cleared
23
+ def needs_clear?
24
+ @needs_clear
25
+ end
26
+
27
+ # Clear the needs_clear flag after redraw
28
+ def clear_needs_clear
29
+ @needs_clear = false
30
+ end
31
+
19
32
  def active?
20
33
  !@items.empty?
21
34
  end
@@ -10,6 +10,8 @@ module Mui
10
10
  SPECIAL_KEYS = {
11
11
  "space" => " ",
12
12
  "tab" => "\t",
13
+ "s-tab" => :shift_tab,
14
+ "btab" => :shift_tab,
13
15
  "cr" => "\r",
14
16
  "enter" => "\r",
15
17
  "return" => "\r",
@@ -92,15 +94,18 @@ module Mui
92
94
  def parse_special(name)
93
95
  return :leader if name.casecmp?("leader")
94
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
+
95
101
  # Handle Ctrl key: <C-x>, <Ctrl-x>, <C-X>
96
102
  return parse_ctrl_key(::Regexp.last_match(2)) if name =~ /\A(c|ctrl)-(.+)\z/i
97
103
 
98
104
  # Handle Shift key: <S-x>, <Shift-x>
99
105
  return parse_shift_key(::Regexp.last_match(2)) if name =~ /\A(s|shift)-(.+)\z/i
100
106
 
101
- # Handle other special keys
102
- normalized_name = name.downcase
103
- SPECIAL_KEYS[normalized_name] || name
107
+ # Unknown special key - return as-is
108
+ name
104
109
  end
105
110
 
106
111
  # Normalize an input key (from terminal) to internal representation
@@ -136,6 +141,8 @@ module Mui
136
141
  "\e"
137
142
  when KeyCode::TAB
138
143
  "\t"
144
+ when 353 # Curses::KEY_BTAB (Shift+Tab)
145
+ :shift_tab
139
146
  when KeyCode::BACKSPACE
140
147
  "\x7f"
141
148
  when 0..31
data/lib/mui/screen.rb CHANGED
@@ -25,6 +25,12 @@ module Mui
25
25
  @adapter.clear
26
26
  end
27
27
 
28
+ # Force a complete redraw of the screen
29
+ # This is needed when multibyte characters may have been corrupted
30
+ def touchwin
31
+ @adapter.touchwin
32
+ end
33
+
28
34
  def put(y, x, text)
29
35
  return if y.negative?
30
36
  return if y >= @height || x >= @width
@@ -62,49 +62,62 @@ module Mui
62
62
  end
63
63
 
64
64
  # Get matches for a specific row in a specific buffer
65
+ # O(1) lookup using row_index
65
66
  def matches_for_row(row, buffer: nil)
66
67
  return [] if buffer.nil?
67
68
 
68
- matches = get_or_calculate_matches(buffer)
69
- matches.select { |m| m[:row] == row }
69
+ cache = get_or_calculate_cache(buffer)
70
+ cache[:row_index][row] || []
70
71
  end
71
72
 
72
73
  private
73
74
 
74
75
  def get_or_calculate_matches(buffer)
76
+ get_or_calculate_cache(buffer)[:matches]
77
+ end
78
+
79
+ def get_or_calculate_cache(buffer)
75
80
  buffer_id = buffer.object_id
76
81
  cached = @buffer_matches[buffer_id]
77
82
 
78
- # Return cached matches if valid (same pattern version and buffer hasn't changed)
79
- return cached[:matches] if cached && cached[:version] == @pattern_version && cached[:change_count] == buffer.change_count
83
+ # Return cached data if valid (same pattern version and buffer hasn't changed)
84
+ return cached if cached && cached[:version] == @pattern_version && cached[:change_count] == buffer.change_count
80
85
 
81
86
  # Calculate and cache matches for this buffer
82
- matches = calculate_matches(buffer)
87
+ matches, row_index = calculate_matches(buffer)
83
88
  @buffer_matches[buffer_id] = {
84
89
  version: @pattern_version,
85
90
  change_count: buffer.change_count,
86
- matches:
91
+ matches:,
92
+ row_index:
87
93
  }
88
- matches
94
+ @buffer_matches[buffer_id]
89
95
  end
90
96
 
91
97
  def calculate_matches(buffer)
92
- return [] if @pattern.nil? || @pattern.empty?
98
+ empty_result = [[], {}]
99
+ return empty_result if @pattern.nil? || @pattern.empty?
93
100
 
94
101
  matches = []
102
+ row_index = {}
95
103
  begin
96
104
  regex = Regexp.new(@pattern)
97
105
  buffer.line_count.times do |row|
98
106
  line = buffer.line(row)
99
- scan_line_matches(matches, line, row, regex)
107
+ row_matches = scan_line_matches(line, row, regex)
108
+ unless row_matches.empty?
109
+ matches.concat(row_matches)
110
+ row_index[row] = row_matches
111
+ end
100
112
  end
101
113
  rescue RegexpError
102
114
  # Invalid regex pattern - no matches
103
115
  end
104
- matches
116
+ [matches, row_index]
105
117
  end
106
118
 
107
- def scan_line_matches(matches, line, row, regex)
119
+ def scan_line_matches(line, row, regex)
120
+ matches = []
108
121
  offset = 0
109
122
  while (match_data = line.match(regex, offset))
110
123
  col = match_data.begin(0)
@@ -116,6 +129,7 @@ module Mui
116
129
  offset += 1 if match_data[0].empty?
117
130
  break if offset >= line.length
118
131
  end
132
+ matches
119
133
  end
120
134
  end
121
135
  end