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
data/lib/mui/window.rb CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Mui
4
4
  class Window
5
- attr_accessor :x, :y, :width, :height, :cursor_row, :cursor_col, :scroll_row, :scroll_col
5
+ # Threshold for using smart scroll jump (in logical rows)
6
+ SMART_JUMP_THRESHOLD = 100
7
+
8
+ attr_accessor :x, :y, :width, :height, :cursor_row, :cursor_col, :scroll_row
6
9
  attr_reader :buffer
7
10
 
8
11
  def initialize(buffer, x: 0, y: 0, width: 80, height: 24, color_scheme: nil)
@@ -14,8 +17,8 @@ module Mui
14
17
  @cursor_row = 0
15
18
  @cursor_col = 0
16
19
  @scroll_row = 0
17
- @scroll_col = 0
18
20
  @color_scheme = color_scheme
21
+ @wrap_cache = WrapCache.new
19
22
  @syntax_highlighter = Highlighters::SyntaxHighlighter.new(color_scheme, buffer:)
20
23
  @line_renderer = create_line_renderer
21
24
  @status_line_renderer = StatusLineRenderer.new(buffer, self, color_scheme)
@@ -26,7 +29,7 @@ module Mui
26
29
  @cursor_row = 0
27
30
  @cursor_col = 0
28
31
  @scroll_row = 0
29
- @scroll_col = 0
32
+ @wrap_cache.clear
30
33
  @syntax_highlighter.buffer = new_buffer
31
34
  @line_renderer = create_line_renderer
32
35
  @status_line_renderer = StatusLineRenderer.new(new_buffer, self, @color_scheme)
@@ -41,47 +44,88 @@ module Mui
41
44
  end
42
45
 
43
46
  def ensure_cursor_visible
44
- # 縦スクロール
45
- if @cursor_row < @scroll_row
46
- @scroll_row = @cursor_row
47
- elsif @cursor_row >= @scroll_row + visible_height
48
- @scroll_row = @cursor_row - visible_height + 1
47
+ # Use smart jump for large cursor movements to avoid O(n) iteration
48
+ distance = (@cursor_row - @scroll_row).abs
49
+ if distance > SMART_JUMP_THRESHOLD
50
+ smart_scroll_to_cursor
51
+ return
49
52
  end
50
53
 
51
- # 横スクロール
52
- if @cursor_col < @scroll_col
53
- @scroll_col = @cursor_col
54
- elsif @cursor_col >= @scroll_col + visible_width
55
- @scroll_col = @cursor_col - visible_width + 1
54
+ # Calculate screen row of cursor considering line wrapping
55
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
56
+
57
+ # Scroll up if cursor is above visible area
58
+ @scroll_row -= 1 while @cursor_row < @scroll_row
59
+
60
+ # Scroll down if cursor is below visible area
61
+ while cursor_screen_row >= visible_height
62
+ @scroll_row += 1
63
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
56
64
  end
57
65
  end
58
66
 
67
+ # Smart scroll: directly position scroll to center cursor on screen
68
+ # Used for large cursor jumps (G, gg, search, etc.) to avoid O(n) iteration
69
+ def smart_scroll_to_cursor
70
+ # Position cursor roughly in the middle of the visible area
71
+ half_visible = visible_height / 2
72
+ target_scroll = @cursor_row - half_visible
73
+ @scroll_row = target_scroll.clamp(0, max_scroll_row)
74
+ end
75
+
59
76
  def render(screen, selection: nil, search_state: nil)
60
77
  options = build_render_options(selection, search_state)
78
+ screen_row = 0
79
+ logical_row = @scroll_row
80
+
81
+ while screen_row < visible_height && logical_row < @buffer.line_count
82
+ line = @buffer.line(logical_row)
83
+ wrapped_lines = WrapHelper.wrap_line(line, visible_width, cache: @wrap_cache)
84
+
85
+ wrapped_lines.each do |wrap_info|
86
+ break if screen_row >= visible_height
87
+
88
+ render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
89
+ screen_row += 1
90
+ end
61
91
 
62
- visible_height.times do |i|
63
- row = @scroll_row + i
64
- render_line(screen, row, i, options)
92
+ logical_row += 1
93
+ end
94
+
95
+ # Clear remaining lines
96
+ while screen_row < visible_height
97
+ clear_line(screen, screen_row)
98
+ screen_row += 1
65
99
  end
66
100
 
67
101
  @status_line_renderer.render(screen, @y + visible_height)
68
102
  end
69
103
 
70
- def render_line(screen, row, screen_row, options)
71
- line = prepare_visible_line(row)
72
- adjusted_options = adjust_options_for_scroll(options)
73
- @line_renderer.render(screen, line, row, @x, @y + screen_row, adjusted_options)
104
+ def render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
105
+ wrap_options = options.merge(logical_row:)
106
+ @line_renderer.render_wrapped_line(screen, @y + screen_row, @x, wrap_info, wrap_options)
107
+
108
+ # Fill remaining width with spaces if line is shorter
109
+ text_width = UnicodeWidth.string_width(wrap_info[:text])
110
+ return unless text_width < visible_width
111
+
112
+ remaining_width = visible_width - text_width
113
+ fill_text = " " * remaining_width
114
+ if @color_scheme && @color_scheme[:normal]
115
+ screen.put_with_style(@y + screen_row, @x + text_width, fill_text, @color_scheme[:normal])
116
+ else
117
+ screen.put(@y + screen_row, @x + text_width, fill_text)
118
+ end
74
119
  end
75
120
 
76
121
  def screen_cursor_x
77
122
  line = @buffer.line(@cursor_row) || ""
78
- # Calculate display width from scroll_col to cursor_col
79
- visible_text = line[@scroll_col...@cursor_col] || ""
80
- @x + UnicodeWidth.string_width(visible_text)
123
+ _, screen_col = WrapHelper.logical_to_screen(line, @cursor_col, visible_width, cache: @wrap_cache)
124
+ @x + screen_col
81
125
  end
82
126
 
83
127
  def screen_cursor_y
84
- @y + @cursor_row - @scroll_row
128
+ @y + screen_rows_from_scroll_to_cursor
85
129
  end
86
130
 
87
131
  # カーソル移動
@@ -115,6 +159,38 @@ module Mui
115
159
 
116
160
  private
117
161
 
162
+ def clear_line(screen, screen_row)
163
+ empty_line = " " * visible_width
164
+ if @color_scheme && @color_scheme[:normal]
165
+ screen.put_with_style(@y + screen_row, @x, empty_line, @color_scheme[:normal])
166
+ else
167
+ screen.put(@y + screen_row, @x, empty_line)
168
+ end
169
+ end
170
+
171
+ # Calculates screen rows from scroll_row to cursor position
172
+ def screen_rows_from_scroll_to_cursor
173
+ screen_rows = 0
174
+
175
+ # Add screen lines for rows between scroll_row and cursor_row
176
+ (@scroll_row...@cursor_row).each do |row|
177
+ line = @buffer.line(row) || ""
178
+ screen_rows += WrapHelper.screen_line_count(line, visible_width, cache: @wrap_cache)
179
+ end
180
+
181
+ # Add the row offset within the cursor's line
182
+ cursor_line = @buffer.line(@cursor_row) || ""
183
+ row_offset, = WrapHelper.logical_to_screen(cursor_line, @cursor_col, visible_width, cache: @wrap_cache)
184
+ screen_rows + row_offset
185
+ end
186
+
187
+ # Clear wrap cache when window dimensions change
188
+ def resize(new_width, new_height)
189
+ @width = new_width
190
+ @height = new_height
191
+ @wrap_cache.clear
192
+ end
193
+
118
194
  def create_line_renderer
119
195
  renderer = LineRenderer.new(@color_scheme)
120
196
  renderer.add_highlighter(@syntax_highlighter)
@@ -129,28 +205,18 @@ module Mui
129
205
  renderer
130
206
  end
131
207
 
132
- def prepare_visible_line(row)
133
- line = @buffer.line(row)
134
- visible_line = @scroll_col < line.length ? line[@scroll_col, visible_width] || "" : ""
135
- visible_line.ljust(visible_width)
136
- end
137
-
138
208
  def build_render_options(selection, search_state)
139
- { selection:, search_state:, scroll_col: @scroll_col, buffer: @buffer }
140
- end
141
-
142
- def adjust_options_for_scroll(options)
143
- return options unless options[:selection] || options[:search_state]
144
-
145
- adjusted = options.dup
146
- adjusted[:scroll_col] = @scroll_col
147
- adjusted
209
+ { selection:, search_state:, buffer: @buffer }
148
210
  end
149
211
 
150
212
  def max_cursor_col
151
213
  [@buffer.line(@cursor_row).length - 1, 0].max
152
214
  end
153
215
 
216
+ def max_scroll_row
217
+ [@buffer.line_count - 1, 0].max
218
+ end
219
+
154
220
  def clamp_cursor_col
155
221
  @cursor_col = max_cursor_col if @cursor_col > max_cursor_col
156
222
  end
@@ -105,6 +105,13 @@ module Mui
105
105
  @active_window = target if target
106
106
  end
107
107
 
108
+ def focus_window(window)
109
+ return false unless windows.include?(window)
110
+
111
+ @active_window = window
112
+ true
113
+ end
114
+
108
115
  def windows
109
116
  return [] unless @layout_root
110
117
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Caches wrap calculation results for performance
5
+ # Cache key: [line_content, width]
6
+ class WrapCache
7
+ def initialize
8
+ @cache = {}
9
+ end
10
+
11
+ def get(line, width)
12
+ key = cache_key(line, width)
13
+ @cache[key]
14
+ end
15
+
16
+ def set(line, width, result)
17
+ key = cache_key(line, width)
18
+ @cache[key] = result
19
+ end
20
+
21
+ def invalidate(line)
22
+ # Remove all entries for this line content
23
+ @cache.delete_if { |k, _| k.start_with?("#{line}\x00") }
24
+ end
25
+
26
+ def clear
27
+ @cache.clear
28
+ end
29
+
30
+ def size
31
+ @cache.size
32
+ end
33
+
34
+ private
35
+
36
+ def cache_key(line, width)
37
+ "#{line}\x00#{width}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ # Helper module for line wrapping calculations
5
+ module WrapHelper
6
+ class << self
7
+ # Wraps a line into screen lines based on display width
8
+ # Returns: [{ text:, start_col:, end_col: }, ...]
9
+ def wrap_line(line, width, cache: nil)
10
+ return [{ text: "", start_col: 0, end_col: 0 }] if line.nil? || line.empty?
11
+ return [{ text: line, start_col: 0, end_col: line.length }] if width <= 0
12
+
13
+ if cache
14
+ cached = cache.get(line, width)
15
+ return cached if cached
16
+ end
17
+
18
+ result = compute_wrap(line, width)
19
+ cache&.set(line, width, result)
20
+ result
21
+ end
22
+
23
+ # Converts logical column to screen position
24
+ # Returns: [screen_row_offset, screen_col]
25
+ def logical_to_screen(line, col, width, cache: nil)
26
+ return [0, 0] if line.nil? || line.empty? || col <= 0
27
+
28
+ wrapped = wrap_line(line, width, cache:)
29
+
30
+ wrapped.each_with_index do |segment, row|
31
+ next unless col <= segment[:end_col]
32
+
33
+ # Found the segment containing this column
34
+ relative_col = col - segment[:start_col]
35
+ prefix = segment[:text][0, relative_col] || ""
36
+ screen_col = UnicodeWidth.string_width(prefix)
37
+ return [row, screen_col]
38
+ end
39
+
40
+ # Column is past end of line, return last position
41
+ last_segment = wrapped.last
42
+ last_text = last_segment[:text]
43
+ [wrapped.size - 1, UnicodeWidth.string_width(last_text)]
44
+ end
45
+
46
+ # Returns the number of screen lines for a logical line
47
+ def screen_line_count(line, width, cache: nil)
48
+ wrap_line(line, width, cache:).size
49
+ end
50
+
51
+ private
52
+
53
+ def compute_wrap(line, width)
54
+ result = []
55
+ chars = line.chars
56
+ current_text = String.new
57
+ current_width = 0
58
+ start_col = 0
59
+ col = 0
60
+
61
+ chars.each do |char|
62
+ char_w = UnicodeWidth.char_width(char)
63
+
64
+ # Check if adding this character would exceed width
65
+ if current_width + char_w > width && !current_text.empty?
66
+ result << { text: current_text, start_col:, end_col: col }
67
+ current_text = String.new
68
+ current_width = 0
69
+ start_col = col
70
+ end
71
+
72
+ current_text << char
73
+ current_width += char_w
74
+ col += 1
75
+ end
76
+
77
+ # Add remaining text
78
+ result << { text: current_text, start_col:, end_col: col } unless current_text.empty?
79
+
80
+ result.empty? ? [{ text: String.new, start_col: 0, end_col: 0 }] : result
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/mui.rb CHANGED
@@ -3,7 +3,14 @@
3
3
  require_relative "mui/version"
4
4
  require_relative "mui/error"
5
5
  require_relative "mui/key_code"
6
+ require_relative "mui/key_notation_parser"
7
+ require_relative "mui/key_sequence"
8
+ require_relative "mui/key_sequence_buffer"
9
+ require_relative "mui/key_sequence_matcher"
10
+ require_relative "mui/key_sequence_handler"
6
11
  require_relative "mui/unicode_width"
12
+ require_relative "mui/wrap_cache"
13
+ require_relative "mui/wrap_helper"
7
14
  require_relative "mui/config"
8
15
  require_relative "mui/color_scheme"
9
16
  require_relative "mui/color_manager"
@@ -22,6 +29,13 @@ require_relative "mui/syntax/token"
22
29
  require_relative "mui/syntax/lexer_base"
23
30
  require_relative "mui/syntax/lexers/ruby_lexer"
24
31
  require_relative "mui/syntax/lexers/c_lexer"
32
+ require_relative "mui/syntax/lexers/go_lexer"
33
+ require_relative "mui/syntax/lexers/rust_lexer"
34
+ require_relative "mui/syntax/lexers/javascript_lexer"
35
+ require_relative "mui/syntax/lexers/typescript_lexer"
36
+ require_relative "mui/syntax/lexers/markdown_lexer"
37
+ require_relative "mui/syntax/lexers/html_lexer"
38
+ require_relative "mui/syntax/lexers/css_lexer"
25
39
  require_relative "mui/syntax/token_cache"
26
40
  require_relative "mui/syntax/language_detector"
27
41
  require_relative "mui/highlighters/syntax_highlighter"
@@ -38,6 +52,7 @@ require_relative "mui/tab_manager"
38
52
  require_relative "mui/tab_bar_renderer"
39
53
  require_relative "mui/mode"
40
54
  require_relative "mui/handler_result"
55
+ require_relative "mui/command_history"
41
56
  require_relative "mui/command_line"
42
57
  require_relative "mui/completion_state"
43
58
  require_relative "mui/insert_completion_state"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS
@@ -38,6 +38,14 @@ files:
38
38
  - LICENSE.txt
39
39
  - README.md
40
40
  - Rakefile
41
+ - docs/_config.yml
42
+ - docs/configuration.md
43
+ - docs/getting-started.md
44
+ - docs/index.md
45
+ - docs/jobs.md
46
+ - docs/keybindings.md
47
+ - docs/plugins.md
48
+ - docs/syntax-highlighting.md
41
49
  - exe/mui
42
50
  - lib/mui.rb
43
51
  - lib/mui/autocmd.rb
@@ -48,6 +56,7 @@ files:
48
56
  - lib/mui/color_scheme.rb
49
57
  - lib/mui/command_completer.rb
50
58
  - lib/mui/command_context.rb
59
+ - lib/mui/command_history.rb
51
60
  - lib/mui/command_line.rb
52
61
  - lib/mui/command_registry.rb
53
62
  - lib/mui/completion_renderer.rb
@@ -84,6 +93,11 @@ files:
84
93
  - lib/mui/key_handler/visual_line_mode.rb
85
94
  - lib/mui/key_handler/visual_mode.rb
86
95
  - lib/mui/key_handler/window_command.rb
96
+ - lib/mui/key_notation_parser.rb
97
+ - lib/mui/key_sequence.rb
98
+ - lib/mui/key_sequence_buffer.rb
99
+ - lib/mui/key_sequence_handler.rb
100
+ - lib/mui/key_sequence_matcher.rb
87
101
  - lib/mui/layout/calculator.rb
88
102
  - lib/mui/layout/leaf_node.rb
89
103
  - lib/mui/layout/node.rb
@@ -104,7 +118,14 @@ files:
104
118
  - lib/mui/syntax/language_detector.rb
105
119
  - lib/mui/syntax/lexer_base.rb
106
120
  - lib/mui/syntax/lexers/c_lexer.rb
121
+ - lib/mui/syntax/lexers/css_lexer.rb
122
+ - lib/mui/syntax/lexers/go_lexer.rb
123
+ - lib/mui/syntax/lexers/html_lexer.rb
124
+ - lib/mui/syntax/lexers/javascript_lexer.rb
125
+ - lib/mui/syntax/lexers/markdown_lexer.rb
107
126
  - lib/mui/syntax/lexers/ruby_lexer.rb
127
+ - lib/mui/syntax/lexers/rust_lexer.rb
128
+ - lib/mui/syntax/lexers/typescript_lexer.rb
108
129
  - lib/mui/syntax/token.rb
109
130
  - lib/mui/syntax/token_cache.rb
110
131
  - lib/mui/tab_bar_renderer.rb
@@ -120,13 +141,15 @@ files:
120
141
  - lib/mui/version.rb
121
142
  - lib/mui/window.rb
122
143
  - lib/mui/window_manager.rb
144
+ - lib/mui/wrap_cache.rb
145
+ - lib/mui/wrap_helper.rb
123
146
  - sig/mui.rbs
124
- homepage: https://github.com/S-H-GAMELINKS/mui
147
+ homepage: https://s-h-gamelinks.github.io/mui/
125
148
  licenses:
126
149
  - MIT
127
150
  metadata:
128
151
  allowed_push_host: https://rubygems.org
129
- homepage_uri: https://github.com/S-H-GAMELINKS/mui
152
+ homepage_uri: https://s-h-gamelinks.github.io/mui/
130
153
  source_code_uri: https://github.com/S-H-GAMELINKS/mui
131
154
  changelog_uri: https://github.com/S-H-GAMELINKS/mui/blob/main/CHANGELOG.md
132
155
  rubygems_mfa_required: 'true'