mui 0.4.1 → 0.5.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -22
  3. data/.ruby-lsp.yml +19 -0
  4. data/CHANGELOG.md +48 -0
  5. data/README.md +17 -0
  6. data/docs/configuration.md +40 -0
  7. data/docs/getting-started.md +1 -1
  8. data/docs/keybindings.md +4 -0
  9. data/lib/mui/buffer.rb +1 -1
  10. data/lib/mui/buffer_word_cache.rb +2 -3
  11. data/lib/mui/command_completer.rb +4 -2
  12. data/lib/mui/command_line.rb +7 -2
  13. data/lib/mui/completion_renderer.rb +1 -1
  14. data/lib/mui/config.rb +3 -1
  15. data/lib/mui/editor.rb +3 -0
  16. data/lib/mui/file_completer.rb +10 -10
  17. data/lib/mui/insert_completion_renderer.rb +1 -1
  18. data/lib/mui/key_code.rb +5 -0
  19. data/lib/mui/key_handler/base.rb +2 -0
  20. data/lib/mui/key_handler/command_mode.rb +1 -1
  21. data/lib/mui/key_handler/insert_mode.rb +30 -0
  22. data/lib/mui/key_handler/normal_mode.rb +21 -0
  23. data/lib/mui/key_handler/visual_mode.rb +3 -1
  24. data/lib/mui/key_notation_parser.rb +13 -4
  25. data/lib/mui/mode_manager.rb +15 -27
  26. data/lib/mui/register.rb +36 -0
  27. data/lib/mui/screen.rb +5 -3
  28. data/lib/mui/search_completer.rb +1 -1
  29. data/lib/mui/search_input.rb +3 -3
  30. data/lib/mui/search_state.rb +11 -2
  31. data/lib/mui/status_line_renderer.rb +3 -5
  32. data/lib/mui/syntax/lexers/markdown_lexer.rb +1 -1
  33. data/lib/mui/terminal_adapter/curses.rb +40 -46
  34. data/lib/mui/unicode_width.rb +1 -1
  35. data/lib/mui/version.rb +1 -1
  36. data/lib/mui/window.rb +8 -0
  37. data/lib/mui/window_manager.rb +2 -5
  38. data/lib/mui/wrap_helper.rb +3 -3
  39. data/sig/lib/mui/autocmd.rbs +12 -0
  40. data/sig/lib/mui/buffer.rbs +52 -0
  41. data/sig/lib/mui/buffer_word_cache.rbs +21 -0
  42. data/sig/lib/mui/buffer_word_completer.rbs +15 -0
  43. data/sig/lib/mui/color_manager.rbs +20 -0
  44. data/sig/lib/mui/color_scheme.rbs +14 -0
  45. data/sig/lib/mui/command_completer.rbs +11 -0
  46. data/sig/lib/mui/command_context.rbs +24 -0
  47. data/sig/lib/mui/command_history.rbs +20 -0
  48. data/sig/lib/mui/command_line.rbs +24 -0
  49. data/sig/lib/mui/command_registry.rbs +14 -0
  50. data/sig/lib/mui/completion_renderer.rbs +14 -0
  51. data/sig/lib/mui/completion_state.rbs +20 -0
  52. data/sig/lib/mui/config.rbs +20 -0
  53. data/sig/lib/mui/editor.rbs +54 -0
  54. data/sig/lib/mui/error.rbs +18 -0
  55. data/sig/lib/mui/file_completer.rbs +13 -0
  56. data/sig/lib/mui/floating_window.rbs +30 -0
  57. data/sig/lib/mui/handler_result.rbs +38 -0
  58. data/sig/lib/mui/highlight.rbs +14 -0
  59. data/sig/lib/mui/highlighters/base.rbs +15 -0
  60. data/sig/lib/mui/highlighters/search_highlighter.rbs +11 -0
  61. data/sig/lib/mui/highlighters/selection_highlighter.rbs +13 -0
  62. data/sig/lib/mui/highlighters/syntax_highlighter.rbs +20 -0
  63. data/sig/lib/mui/input.rbs +10 -0
  64. data/sig/lib/mui/insert_completion_renderer.rbs +15 -0
  65. data/sig/lib/mui/insert_completion_state.rbs +23 -0
  66. data/sig/lib/mui/job.rbs +27 -0
  67. data/sig/lib/mui/job_manager.rbs +20 -0
  68. data/sig/lib/mui/key_code.rbs +29 -0
  69. data/sig/lib/mui/key_handler/base.rbs +35 -0
  70. data/sig/lib/mui/key_handler/command_mode.rbs +56 -0
  71. data/sig/lib/mui/key_handler/insert_mode.rbs +42 -0
  72. data/sig/lib/mui/key_handler/motions/motion_handler.rbs +19 -0
  73. data/sig/lib/mui/key_handler/normal_mode.rbs +55 -0
  74. data/sig/lib/mui/key_handler/operators/base_operator.rbs +29 -0
  75. data/sig/lib/mui/key_handler/operators/change_operator.rbs +22 -0
  76. data/sig/lib/mui/key_handler/operators/delete_operator.rbs +22 -0
  77. data/sig/lib/mui/key_handler/operators/paste_operator.rbs +19 -0
  78. data/sig/lib/mui/key_handler/operators/yank_operator.rbs +20 -0
  79. data/sig/lib/mui/key_handler/search_mode.rbs +25 -0
  80. data/sig/lib/mui/key_handler/visual_line_mode.rbs +11 -0
  81. data/sig/lib/mui/key_handler/visual_mode.rbs +43 -0
  82. data/sig/lib/mui/key_handler/window_command.rbs +20 -0
  83. data/sig/lib/mui/key_handler.rbs +4 -0
  84. data/sig/lib/mui/key_notation_parser.rbs +16 -0
  85. data/sig/lib/mui/key_sequence.rbs +18 -0
  86. data/sig/lib/mui/key_sequence_buffer.rbs +18 -0
  87. data/sig/lib/mui/key_sequence_handler.rbs +24 -0
  88. data/sig/lib/mui/key_sequence_matcher.rbs +14 -0
  89. data/sig/lib/mui/layout/calculator.rbs +10 -0
  90. data/sig/lib/mui/layout/leaf_node.rbs +16 -0
  91. data/sig/lib/mui/layout/node.rbs +24 -0
  92. data/sig/lib/mui/layout/split_node.rbs +29 -0
  93. data/sig/lib/mui/line_renderer.rbs +18 -0
  94. data/sig/lib/mui/mode.rbs +14 -0
  95. data/sig/lib/mui/mode_manager.rbs +33 -0
  96. data/sig/lib/mui/motion.rbs +24 -0
  97. data/sig/lib/mui/plugin.rbs +17 -0
  98. data/sig/lib/mui/plugin_manager.rbs +20 -0
  99. data/sig/lib/mui/register.rbs +21 -0
  100. data/sig/lib/mui/screen.rbs +21 -0
  101. data/sig/lib/mui/search_completer.rbs +11 -0
  102. data/sig/lib/mui/search_input.rbs +17 -0
  103. data/sig/lib/mui/search_state.rbs +22 -0
  104. data/sig/lib/mui/selection.rbs +21 -0
  105. data/sig/lib/mui/status_line_renderer.rbs +12 -0
  106. data/sig/lib/mui/syntax/language_detector.rbs +16 -0
  107. data/sig/lib/mui/syntax/lexer_base.rbs +16 -0
  108. data/sig/lib/mui/syntax/lexers/c_lexer.rbs +19 -0
  109. data/sig/lib/mui/syntax/lexers/css_lexer.rbs +19 -0
  110. data/sig/lib/mui/syntax/lexers/go_lexer.rbs +26 -0
  111. data/sig/lib/mui/syntax/lexers/html_lexer.rbs +19 -0
  112. data/sig/lib/mui/syntax/lexers/javascript_lexer.rbs +26 -0
  113. data/sig/lib/mui/syntax/lexers/markdown_lexer.rbs +24 -0
  114. data/sig/lib/mui/syntax/lexers/ruby_lexer.rbs +18 -0
  115. data/sig/lib/mui/syntax/lexers/rust_lexer.rbs +21 -0
  116. data/sig/lib/mui/syntax/lexers/typescript_lexer.rbs +26 -0
  117. data/sig/lib/mui/syntax/token.rbs +16 -0
  118. data/sig/lib/mui/syntax/token_cache.rbs +16 -0
  119. data/sig/lib/mui/tab_bar_renderer.rbs +23 -0
  120. data/sig/lib/mui/tab_manager.rbs +23 -0
  121. data/sig/lib/mui/tab_page.rbs +16 -0
  122. data/sig/lib/mui/terminal_adapter/base.rbs +31 -0
  123. data/sig/lib/mui/terminal_adapter/curses.rbs +31 -0
  124. data/sig/lib/mui/terminal_adapter.rbs +4 -0
  125. data/sig/lib/mui/themes/default.rbs +15 -0
  126. data/sig/lib/mui/undo_manager.rbs +20 -0
  127. data/sig/lib/mui/undoable_action.rbs +56 -0
  128. data/sig/lib/mui/unicode_width.rbs +12 -0
  129. data/sig/lib/mui/version.rbs +6 -0
  130. data/sig/lib/mui/window.rbs +49 -0
  131. data/sig/lib/mui/window_manager.rbs +37 -0
  132. data/sig/lib/mui/wrap_cache.rbs +14 -0
  133. data/sig/lib/mui/wrap_helper.rbs +11 -0
  134. data/sig/lib/mui.rbs +24 -0
  135. data/sig/typeprof.rbs +42 -0
  136. data/typeprof.conf.jsonc +6 -0
  137. metadata +116 -4
  138. data/sig/mui.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3555a781cf03a72f45aece354917a6221b12915deb638589fb3678d21764e16
4
- data.tar.gz: 64ffe746691655d9e479e263ec023953f1a6fae20a9984cffea2da323c845fd0
3
+ metadata.gz: 673cc3f184e93e2d2d0ce36bf723c2bdc8cabe688c8191df01dfbe8902c1b9f8
4
+ data.tar.gz: fa1b949c989cac09566b7f55dd7f6ac2f5d57fc593d638c69973d6b86c0be515
5
5
  SHA512:
6
- metadata.gz: 4b3cd55455549c90e5b229d028e12b735f50c4cb1e5bd7e0fb7f84e3d5fc7f2a82097ac10e50cf0b52a304076460adab2e6b01671cfd32629f225888d4a8ff8c
7
- data.tar.gz: ee99312173108b0efc849791a426ca247317999cf2b0b5ec3e99e3cbf120086b945f85c8fa46db25a59cd24e7b5784b1414465fd8b4a71bea37b4641fc88c64e
6
+ metadata.gz: 9d9bc8288b7d1947a7946d192243c9b30b307e05b6142699958e8b2b34a986324f2b07d5125d779e919eecff84a12965b23ca5558bf6048c386d26853484cebe
7
+ data.tar.gz: bb9fe3e3fbb7d0db5371ba822457e5d215198d1c5cd2c2269f73e9d65da834d699089425d8baed932f9fc0ec56deb23da8f453fbf8aac012e5f713d7499991c7
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-18 10:56:40 UTC using RuboCop version 1.81.7.
3
+ # on 2025-12-24 03:48:47 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
@@ -21,13 +21,6 @@ Lint/DuplicateBranch:
21
21
  - 'lib/mui/plugin_manager.rb'
22
22
  - 'lib/mui/register.rb'
23
23
 
24
- # Offense count: 3
25
- # Configuration parameters: AllowComments, AllowEmptyLambdas.
26
- Lint/EmptyBlock:
27
- Exclude:
28
- - 'test/mui/test_autocmd.rb'
29
- - 'test/mui/test_command_registry.rb'
30
-
31
24
  # Offense count: 1
32
25
  # Configuration parameters: AllowedParentClasses.
33
26
  Lint/MissingSuper:
@@ -43,15 +36,10 @@ Lint/UnusedMethodArgument:
43
36
  - 'lib/mui/terminal_adapter/base.rb'
44
37
  - 'test/mui/highlighters/test_search_highlighter.rb'
45
38
 
46
- # Offense count: 1
47
- Lint/UselessConstantScoping:
48
- Exclude:
49
- - 'lib/mui/buffer_word_cache.rb'
50
-
51
39
  # Offense count: 35
52
40
  # Configuration parameters: AllowedMethods, AllowedPatterns.
53
41
  Metrics/CyclomaticComplexity:
54
- Max: 49
42
+ Max: 51
55
43
 
56
44
  # Offense count: 2
57
45
  # Configuration parameters: CountComments, CountAsOne.
@@ -156,11 +144,3 @@ Style/ComparableClamp:
156
144
  # Configuration parameters: AllowedConstants.
157
145
  Style/Documentation:
158
146
  Enabled: false
159
-
160
- # Offense count: 1
161
- # This cop supports safe autocorrection (--autocorrect).
162
- # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods.
163
- # AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
164
- Style/TrivialAccessors:
165
- Exclude:
166
- - 'test/mui/test_autocmd.rb'
data/.ruby-lsp.yml ADDED
@@ -0,0 +1,19 @@
1
+ # Ruby LSP configuration for Mui
2
+ # https://shopify.github.io/ruby-lsp/
3
+
4
+ indexing:
5
+ # Include all lib files for indexing
6
+ included_patterns:
7
+ - "lib/**/*.rb"
8
+
9
+ # Exclude test files from indexing to speed up
10
+ excluded_patterns:
11
+ - "test/**/*.rb"
12
+ - "test/fixtures/**/*.rb"
13
+
14
+ # Exclude development-only gems from indexing
15
+ excluded_gems:
16
+ - rubocop
17
+ - rubocop-rake
18
+ - rubocop-minitest
19
+ - rubocop-performance
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2025-12-26
4
+
5
+ ### Added
6
+ - Ruby 4.0 support in CI matrix
7
+ - System clipboard integration:
8
+ - `clipboard` setting (`:unnamed` or `:unnamedplus`) to enable
9
+ - Yank operations (`yy`, `yw`, `y` in Visual mode) sync to system clipboard
10
+ - Delete operations (`dd`, `dw`, `d` in Visual mode) sync to system clipboard
11
+ - Paste operations (`p`, `P`) sync from system clipboard
12
+ - Linewise detection: trailing newline indicates line-wise content
13
+ - Named registers (`"a`-`"z`) and black hole register (`"_`) do not sync to clipboard
14
+ - Requires `clipboard` gem (automatically installed as dependency)
15
+ - YJIT support (Ruby 3.3+):
16
+ - `use_yjit` configuration option (default: `true`)
17
+ - Automatically enables YJIT at editor startup when available
18
+ - Disable with `Mui.set :use_yjit, false` in `.muirc`
19
+ - Shift+Arrow key navigation across lines:
20
+ - `Shift+Left` at line start moves to end of previous line
21
+ - `Shift+Right` at line end moves to start of next line
22
+ - Works in both Normal mode and Insert mode
23
+ - Boundary behavior: does nothing at first/last line
24
+
25
+ ### Fixed
26
+ - Fixed Shift+Tab handling causing `NoMethodError` in command mode completion:
27
+ - `:shift_tab` symbol was passed to `convert_key_to_string` which called `chr` on it
28
+ - Unified key representation: `KeyCode::SHIFT_TAB = 353` (same as `Curses::KEY_BTAB`)
29
+ - Added `SHIFT_CHARS` mapping similar to `CTRL_CHARS` for Shift key combinations
30
+ - All Shift+Tab representations now use `"\T"` string consistently
31
+
32
+ ### Changed
33
+ - FileCompleter now includes hidden files (dotfiles) in completion candidates
34
+ - Previously, files starting with `.` were excluded from file path completion
35
+ - Now returns all entries including `.`, `..`, and hidden files like `.gitignore`
36
+ - Performance improvements across multiple modules:
37
+ - String concatenation: Use `<<` instead of `+` for buffer operations (`command_line.rb`, `search_input.rb`)
38
+ - Status line rendering: Use `ljust` instead of manual padding (`status_line_renderer.rb`)
39
+ - Text truncation: Add `ascii_only?` early return and index-based slicing (`screen.rb`)
40
+ - Method chains: Use `filter_map` instead of `select.map.map` (`file_completer.rb`)
41
+ - Max width calculation: Use `reduce` instead of `map.max` (`completion_renderer.rb`, `insert_completion_renderer.rb`)
42
+ - Regex caching: Cache compiled regex when pattern unchanged (`search_state.rb`)
43
+ - Regex optimization: Add `/o` option for static patterns (`search_completer.rb`)
44
+ - Distance calculation: Remove unnecessary `Math.sqrt` (`window_manager.rb`)
45
+ - Command list: Memoize `all_commands` array (`command_completer.rb`)
46
+
47
+ ### Removed
48
+ - Dropped Ruby 3.2 support (EOL: 2025-03-31)
49
+ - Minimum required Ruby version is now 3.3.0
50
+
3
51
  ## [0.4.1] 2025-12-20
4
52
 
5
53
  ### Fixed
data/README.md CHANGED
@@ -72,6 +72,20 @@ Supports 9 languages:
72
72
  - Delete history: `"1` - `"9`
73
73
  - Black hole register: `"_`
74
74
 
75
+ ### Clipboard Integration
76
+
77
+ System clipboard integration (Vim's `clipboard` option):
78
+
79
+ ```ruby
80
+ # Enable clipboard integration in ~/.muirc
81
+ Mui.set :clipboard, :unnamedplus
82
+ ```
83
+
84
+ - Yank/delete operations sync to system clipboard
85
+ - Paste operations sync from system clipboard
86
+ - Named registers (`"a`-`"z`) are not affected
87
+ - Supports WSL, macOS, and Linux (X11/Wayland)
88
+
75
89
  ### Completion
76
90
 
77
91
  - Command-line completion with popup
@@ -168,6 +182,9 @@ Mui.set :syntax, true
168
182
  # Leader key
169
183
  Mui.set :leader, " "
170
184
 
185
+ # YJIT (enabled by default on Ruby 3.3+)
186
+ Mui.set :use_yjit, true
187
+
171
188
  # Custom keymaps
172
189
  Mui.keymap :normal, "<Leader>w" do |ctx|
173
190
  ctx.editor.execute_command("w")
@@ -92,6 +92,40 @@ Mui.set :leader, " " # Space as leader
92
92
  Mui.set :timeoutlen, 1000
93
93
  ```
94
94
 
95
+ ### YJIT
96
+
97
+ ```ruby
98
+ # Enable/disable YJIT (default: true, Ruby 3.3+ only)
99
+ Mui.set :use_yjit, true
100
+ ```
101
+
102
+ YJIT is Ruby's Just-In-Time compiler that improves performance. When enabled, Mui automatically activates YJIT at startup if your Ruby version supports it (Ruby 3.3+). On older Ruby versions, this setting is safely ignored.
103
+
104
+ ### Clipboard
105
+
106
+ ```ruby
107
+ # Enable system clipboard integration
108
+ Mui.set :clipboard, :unnamedplus
109
+ ```
110
+
111
+ | Value | Description |
112
+ |-------|-------------|
113
+ | `nil` | Disabled (default) |
114
+ | `:unnamed` | Sync with system clipboard |
115
+ | `:unnamedplus` | Sync with system clipboard (same as `:unnamed`) |
116
+
117
+ When enabled:
118
+ - **Yank** (`yy`, `yw`, `y` in Visual mode) copies to system clipboard
119
+ - **Delete** (`dd`, `dw`, `d` in Visual mode) copies to system clipboard
120
+ - **Paste** (`p`, `P`) reads from system clipboard
121
+ - **Named registers** (`"a`-`"z`) are not affected by clipboard sync
122
+ - **Black hole register** (`"_`) discards without clipboard sync
123
+
124
+ Platform support (via `clipboard` gem):
125
+ - **macOS**: Uses `pbcopy`/`pbpaste`
126
+ - **Linux**: Uses `xclip` or `xsel` (X11) / `wl-copy`/`wl-paste` (Wayland)
127
+ - **WSL**: Uses `clip.exe` and PowerShell
128
+
95
129
  ---
96
130
 
97
131
  ## Custom Key Mappings
@@ -279,6 +313,12 @@ Mui.set :expandtab, true
279
313
  # Leader key
280
314
  Mui.set :leader, " "
281
315
 
316
+ # YJIT (Ruby 3.3+)
317
+ Mui.set :use_yjit, true
318
+
319
+ # Clipboard integration
320
+ Mui.set :clipboard, :unnamedplus
321
+
282
322
  # Plugins
283
323
  Mui.use "mui-lsp"
284
324
  Mui.use "mui-fzf"
@@ -25,7 +25,7 @@ gem install mui
25
25
 
26
26
  ### Requirements
27
27
 
28
- - Ruby 3.0 or later
28
+ - Ruby 3.3 or later
29
29
  - A terminal that supports 256 colors
30
30
 
31
31
  ## Basic Usage
data/docs/keybindings.md CHANGED
@@ -28,6 +28,8 @@ nav_order: 3
28
28
  | `0` | Move to line start |
29
29
  | `^` | Move to first non-blank character |
30
30
  | `$` | Move to line end |
31
+ | `Shift-Left` | Move to end of previous line |
32
+ | `Shift-Right` | Move to start of next line |
31
33
 
32
34
  ### Word Movement
33
35
 
@@ -144,6 +146,8 @@ Example: `"ayy` yanks line to register `a`, `"ap` pastes from register `a`.
144
146
  | `Ctrl-p` | Previous completion candidate |
145
147
  | `Tab` | Accept completion |
146
148
  | Arrow keys | Move cursor |
149
+ | `Shift-Left` | Move to end of previous line |
150
+ | `Shift-Right` | Move to start of next line |
147
151
  | `Backspace` | Delete character before cursor |
148
152
 
149
153
  ---
data/lib/mui/buffer.rb CHANGED
@@ -240,7 +240,7 @@ module Mui
240
240
  end
241
241
 
242
242
  def empty_line
243
- String.new
243
+ +""
244
244
  end
245
245
 
246
246
  def capture_range(start_row, start_col, end_row, end_col)
@@ -8,6 +8,8 @@ module Mui
8
8
  MIN_WORD_LENGTH = 2
9
9
  # Maximum candidates to return (for performance)
10
10
  MAX_CANDIDATES = 50
11
+ # Regex to match word characters (alphanumeric + underscore)
12
+ WORD_REGEX = /\w+/
11
13
 
12
14
  def initialize(buffer)
13
15
  @buffer = buffer
@@ -72,9 +74,6 @@ module Mui
72
74
 
73
75
  private
74
76
 
75
- # Regex to match word characters (alphanumeric + underscore)
76
- WORD_REGEX = /\w+/
77
-
78
77
  def build_cache
79
78
  @buffer.lines.each do |line|
80
79
  extract_words_fast(line)
@@ -13,8 +13,6 @@ module Mui
13
13
  ].freeze
14
14
 
15
15
  def complete(prefix)
16
- all_commands = COMMANDS + plugin_command_names
17
-
18
16
  return all_commands.uniq.sort if prefix.empty?
19
17
 
20
18
  prefix_downcase = prefix.downcase
@@ -23,6 +21,10 @@ module Mui
23
21
 
24
22
  private
25
23
 
24
+ def all_commands
25
+ @all_commands ||= COMMANDS + plugin_command_names
26
+ end
27
+
26
28
  def plugin_command_names
27
29
  Mui.config.commands.keys.map(&:to_s)
28
30
  end
@@ -14,14 +14,19 @@ module Mui
14
14
  end
15
15
 
16
16
  def input(char)
17
- @buffer = @buffer[0...@cursor_pos].to_s + char + @buffer[@cursor_pos..].to_s
17
+ rest = @buffer[@cursor_pos..].to_s
18
+ @buffer = @buffer[0...@cursor_pos].to_s
19
+ @buffer << char
20
+ @buffer << rest
18
21
  @cursor_pos += char.length
19
22
  end
20
23
 
21
24
  def backspace
22
25
  return if @cursor_pos.zero?
23
26
 
24
- @buffer = @buffer[0...(@cursor_pos - 1)].to_s + @buffer[@cursor_pos..].to_s
27
+ rest = @buffer[@cursor_pos..].to_s
28
+ @buffer = @buffer[0...(@cursor_pos - 1)].to_s
29
+ @buffer << rest
25
30
  @cursor_pos -= 1
26
31
  end
27
32
 
@@ -67,7 +67,7 @@ module Mui
67
67
  def calculate_max_width(candidates)
68
68
  return 0 if candidates.empty?
69
69
 
70
- candidates.map { |c| display_width(c) }.max + 2 # +2 for padding
70
+ candidates.reduce(0) { |max, c| [max, display_width(c)].max } + 2 # +2 for padding
71
71
  end
72
72
 
73
73
  def display_width(text)
data/lib/mui/config.rb CHANGED
@@ -13,7 +13,9 @@ module Mui
13
13
  tabstop: 8, # Tab display width
14
14
  reselect_after_indent: false, # Keep selection after > / < in visual mode
15
15
  leader: "\\", # Leader key for key mappings (default: backslash)
16
- timeoutlen: 1000 # Timeout for multi-key sequences in milliseconds
16
+ timeoutlen: 1000, # Timeout for multi-key sequences in milliseconds
17
+ use_yjit: true, # Enable YJIT
18
+ clipboard: nil # Clipboard integration (:unnamed or :unnamedplus to enable)
17
19
  }
18
20
  @plugins = []
19
21
  @keymaps = {}
data/lib/mui/editor.rb CHANGED
@@ -10,6 +10,9 @@ module Mui
10
10
  def initialize(file_path = nil, adapter: TerminalAdapter::Curses.new, load_config: true)
11
11
  Mui.load_config if load_config
12
12
 
13
+ # Enable YJIT for Ruby 3.3+
14
+ RubyVM::YJIT.enable if Mui.config.get(:use_yjit) && defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
15
+
13
16
  @adapter = adapter
14
17
  @color_manager = ColorManager.new
15
18
  @adapter.color_resolver = @color_manager
@@ -9,9 +9,9 @@ module Mui
9
9
  dir, prefix = split_path(partial_path)
10
10
  entries = list_directory(dir)
11
11
 
12
- entries.select { |entry| entry.start_with?(prefix) }
13
- .map { |entry| join_path(dir, entry) }
14
- .map { |path| format_path(path) }
12
+ entries.filter_map do |entry|
13
+ format_entry_to_path(dir, entry) if entry.start_with?(prefix)
14
+ end
15
15
  end
16
16
 
17
17
  private
@@ -30,22 +30,22 @@ module Mui
30
30
  target = dir.empty? ? "." : dir
31
31
  return [] unless Dir.exist?(target)
32
32
 
33
- Dir.entries(target)
34
- .reject { |e| e.start_with?(".") }
35
- .sort
33
+ Dir.entries(target).sort
36
34
  end
37
35
 
38
36
  def list_current_directory
39
37
  list_directory("").map { |entry| format_path(entry) }
40
38
  end
41
39
 
42
- def join_path(dir, entry)
43
- dir.empty? ? entry : File.join(dir, entry)
44
- end
45
-
46
40
  def format_path(path)
47
41
  full_path = path.start_with?("/") ? path : File.join(".", path)
48
42
  File.directory?(full_path) ? "#{path}/" : path
49
43
  end
44
+
45
+ def format_entry_to_path(dir, entry)
46
+ path = dir.empty? ? entry : File.join(dir, entry)
47
+
48
+ format_path(path)
49
+ end
50
50
  end
51
51
  end
@@ -70,7 +70,7 @@ module Mui
70
70
  def calculate_max_width(items)
71
71
  return 0 if items.empty?
72
72
 
73
- items.map { |item| display_width(item_label(item)) }.max + 2 # +2 for padding
73
+ items.reduce(0) { |max, item| [max, display_width(item_label(item))].max } + 2 # +2 for padding
74
74
  end
75
75
 
76
76
  def item_label(item)
data/lib/mui/key_code.rb CHANGED
@@ -26,5 +26,10 @@ module Mui
26
26
  CTRL_S = 19
27
27
  CTRL_V = 22
28
28
  CTRL_W = 23
29
+
30
+ # Shift key codes (Shift+letter)
31
+ SHIFT_LEFT = 393
32
+ SHIFT_RIGHT = 402
33
+ SHIFT_TAB = 353
29
34
  end
30
35
  end
@@ -112,6 +112,8 @@ module Mui
112
112
  "\e"
113
113
  when KeyCode::TAB
114
114
  "\t"
115
+ when KeyCode::SHIFT_TAB
116
+ "\T"
115
117
  when KeyCode::BACKSPACE
116
118
  "\x7f"
117
119
  else
@@ -28,7 +28,7 @@ module Mui
28
28
  handle_enter
29
29
  when KeyCode::TAB
30
30
  handle_tab
31
- when Curses::KEY_BTAB
31
+ when KeyCode::SHIFT_TAB
32
32
  handle_shift_tab
33
33
  when Curses::KEY_LEFT
34
34
  handle_cursor_left
@@ -40,6 +40,10 @@ module Mui
40
40
  handle_backspace
41
41
  when KeyCode::ENTER_CR, KeyCode::ENTER_LF, Curses::KEY_ENTER
42
42
  handle_enter
43
+ when KeyCode::SHIFT_LEFT
44
+ handle_shift_left
45
+ when KeyCode::SHIFT_RIGHT
46
+ handle_shift_right
43
47
  else
44
48
  handle_character_input(key)
45
49
  end
@@ -91,6 +95,32 @@ module Mui
91
95
  result
92
96
  end
93
97
 
98
+ def handle_line_start
99
+ window.move_to_line_start
100
+ result
101
+ end
102
+
103
+ def handle_line_end
104
+ window.move_to_line_end
105
+ result
106
+ end
107
+
108
+ def handle_shift_left
109
+ return result unless cursor_row.positive?
110
+
111
+ window.move_up
112
+ window.move_to_line_end
113
+ result
114
+ end
115
+
116
+ def handle_shift_right
117
+ return result unless cursor_row < buffer.line_count - 1
118
+
119
+ window.move_down
120
+ window.move_to_line_start
121
+ result
122
+ end
123
+
94
124
  def handle_backspace
95
125
  if cursor_col.positive?
96
126
  self.cursor_col = cursor_col - 1
@@ -179,6 +179,10 @@ module Mui
179
179
  when KeyCode::CTRL_W
180
180
  @pending_motion = :window_command
181
181
  result
182
+ when KeyCode::SHIFT_LEFT
183
+ handle_shift_left
184
+ when KeyCode::SHIFT_RIGHT
185
+ handle_shift_right
182
186
  else
183
187
  result
184
188
  end
@@ -264,6 +268,23 @@ module Mui
264
268
  result
265
269
  end
266
270
 
271
+ def handle_shift_left
272
+ return result unless cursor_row.positive?
273
+
274
+ window.move_up
275
+ # Normal mode: position at last character (not after it)
276
+ self.cursor_col = [current_line_length - 1, 0].max
277
+ result
278
+ end
279
+
280
+ def handle_shift_right
281
+ return result unless cursor_row < buffer.line_count - 1
282
+
283
+ window.move_down
284
+ window.move_to_line_start
285
+ result
286
+ end
287
+
267
288
  # Edit handlers
268
289
  def handle_append
269
290
  self.cursor_col = cursor_col + 1 if current_line_length.positive?
@@ -6,6 +6,8 @@ module Mui
6
6
  class VisualMode < Base
7
7
  include Motions::MotionHandler
8
8
 
9
+ INDENT_PATTERN = [" ", "\t"].freeze
10
+
9
11
  attr_reader :selection
10
12
 
11
13
  def initialize(mode_manager, buffer, selection, register = nil, undo_manager: nil)
@@ -310,7 +312,7 @@ module Mui
310
312
 
311
313
  while removed < width && !line.empty?
312
314
  char = line[0]
313
- break unless [" ", "\t"].include?(char)
315
+ break unless INDENT_PATTERN.include?(char)
314
316
 
315
317
  char_width = char == "\t" ? Mui.config.get(:tabstop) : 1
316
318
  break if removed + char_width > width && char == "\t"
@@ -10,8 +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
+ "s-tab" => "\T", # Shift+Tab (SHIFT_CHARS["tab"])
14
+ "btab" => "\T", # Back Tab = Shift+Tab
15
15
  "cr" => "\r",
16
16
  "enter" => "\r",
17
17
  "return" => "\r",
@@ -64,6 +64,11 @@ module Mui
64
64
  "_" => 31
65
65
  }.freeze
66
66
 
67
+ # Shift key mappings for special keys
68
+ SHIFT_CHARS = {
69
+ "tab" => "\T" # Shift+Tab
70
+ }.freeze
71
+
67
72
  class << self
68
73
  # Parse a key notation string into an array of keys
69
74
  # @param notation [String] Key notation (e.g., "<Leader>gd", "<C-x><C-s>")
@@ -129,6 +134,10 @@ module Mui
129
134
  end
130
135
 
131
136
  def parse_shift_key(char)
137
+ char_lower = char.downcase
138
+ # Check SHIFT_CHARS for special key mappings (e.g., Shift+Tab)
139
+ return SHIFT_CHARS[char_lower] if SHIFT_CHARS.key?(char_lower)
140
+
132
141
  # Shift typically produces uppercase for letters
133
142
  char.length == 1 ? char.upcase : char
134
143
  end
@@ -141,8 +150,8 @@ module Mui
141
150
  "\e"
142
151
  when KeyCode::TAB
143
152
  "\t"
144
- when 353 # Curses::KEY_BTAB (Shift+Tab)
145
- :shift_tab
153
+ when KeyCode::SHIFT_TAB
154
+ "\T"
146
155
  when KeyCode::BACKSPACE
147
156
  "\x7f"
148
157
  when 0..31
@@ -47,11 +47,11 @@ module Mui
47
47
  when Mode::VISUAL, Mode::VISUAL_LINE
48
48
  handle_visual_transition(result)
49
49
  when Mode::INSERT
50
- handle_insert_transition(result)
50
+ handle_insert_transition(result, result.mode)
51
51
  when Mode::SEARCH_FORWARD
52
- handle_search_forward_transition
52
+ handle_search_transition("/", result.mode)
53
53
  when Mode::SEARCH_BACKWARD
54
- handle_search_backward_transition
54
+ handle_search_transition("?", result.mode)
55
55
  else
56
56
  @mode = result.mode
57
57
  end
@@ -81,11 +81,7 @@ module Mui
81
81
  @last_visual_selection[:end_row],
82
82
  @last_visual_selection[:end_col]
83
83
  )
84
- @visual_handler = if line_mode
85
- KeyHandler::VisualLineMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
86
- else
87
- KeyHandler::VisualMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
88
- end
84
+ @visual_handler = create_visual_handler(line_mode:)
89
85
 
90
86
  # Move cursor to end of selection
91
87
  active_window.cursor_row = @last_visual_selection[:end_row]
@@ -110,10 +106,10 @@ module Mui
110
106
  KeyHandler::InsertMode.new(self, @buffer, undo_manager: @undo_manager, group_started:)
111
107
  end
112
108
 
113
- def handle_insert_transition(result)
109
+ def handle_insert_transition(result, mode)
114
110
  group_started = result.respond_to?(:group_started?) && result.group_started?
115
- @key_handlers[Mode::INSERT] = create_insert_handler(group_started:)
116
- @mode = Mode::INSERT
111
+ @key_handlers[mode] = create_insert_handler(group_started:)
112
+ @mode = mode
117
113
  end
118
114
 
119
115
  def handle_visual_transition(result)
@@ -128,6 +124,7 @@ module Mui
128
124
 
129
125
  def start_visual_mode(mode, line_mode)
130
126
  @mode = mode
127
+ @selection = Selection.new(active_window.cursor_row, active_window.cursor_col, line_mode:)
131
128
  @visual_handler = create_visual_handler(line_mode:)
132
129
  end
133
130
 
@@ -152,16 +149,14 @@ module Mui
152
149
  def toggle_visual_line_mode(new_mode)
153
150
  return unless @selection
154
151
 
155
- new_line_mode = new_mode == Mode::VISUAL_LINE
156
- @selection = Selection.new(@selection.start_row, @selection.start_col, line_mode: new_line_mode)
152
+ line_mode = new_mode == Mode::VISUAL_LINE
153
+ @selection = Selection.new(@selection.start_row, @selection.start_col, line_mode:)
157
154
  @selection.update_end(active_window.cursor_row, active_window.cursor_col)
158
- @visual_handler = create_visual_handler(line_mode: new_line_mode)
155
+ @visual_handler = create_visual_handler(line_mode:)
159
156
  @mode = new_mode
160
157
  end
161
158
 
162
159
  def create_visual_handler(line_mode:)
163
- @selection = Selection.new(active_window.cursor_row, active_window.cursor_col, line_mode:)
164
-
165
160
  if line_mode
166
161
  KeyHandler::VisualLineMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
167
162
  else
@@ -169,18 +164,11 @@ module Mui
169
164
  end
170
165
  end
171
166
 
172
- def handle_search_forward_transition
167
+ def handle_search_transition(prompt, mode)
173
168
  @search_input.clear
174
- @search_input.set_prompt("/")
175
- @key_handlers[Mode::SEARCH_FORWARD].start_search
176
- @mode = Mode::SEARCH_FORWARD
177
- end
178
-
179
- def handle_search_backward_transition
180
- @search_input.clear
181
- @search_input.set_prompt("?")
182
- @key_handlers[Mode::SEARCH_BACKWARD].start_search
183
- @mode = Mode::SEARCH_BACKWARD
169
+ @search_input.set_prompt(prompt)
170
+ @key_handlers[mode].start_search
171
+ @mode = mode
184
172
  end
185
173
  end
186
174
  end