mui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +13 -8
  3. data/CHANGELOG.md +99 -0
  4. data/README.md +309 -6
  5. data/docs/_config.yml +56 -0
  6. data/docs/configuration.md +301 -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 +149 -0
  13. data/lib/mui/command_completer.rb +11 -2
  14. data/lib/mui/command_history.rb +89 -0
  15. data/lib/mui/command_line.rb +32 -2
  16. data/lib/mui/command_registry.rb +21 -2
  17. data/lib/mui/config.rb +3 -1
  18. data/lib/mui/editor.rb +78 -2
  19. data/lib/mui/handler_result.rb +13 -7
  20. data/lib/mui/highlighters/search_highlighter.rb +2 -1
  21. data/lib/mui/highlighters/syntax_highlighter.rb +3 -1
  22. data/lib/mui/key_handler/base.rb +87 -0
  23. data/lib/mui/key_handler/command_mode.rb +68 -0
  24. data/lib/mui/key_handler/insert_mode.rb +10 -41
  25. data/lib/mui/key_handler/normal_mode.rb +24 -51
  26. data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
  27. data/lib/mui/key_handler/search_mode.rb +10 -7
  28. data/lib/mui/key_handler/visual_mode.rb +15 -10
  29. data/lib/mui/key_notation_parser.rb +152 -0
  30. data/lib/mui/key_sequence.rb +67 -0
  31. data/lib/mui/key_sequence_buffer.rb +85 -0
  32. data/lib/mui/key_sequence_handler.rb +163 -0
  33. data/lib/mui/key_sequence_matcher.rb +79 -0
  34. data/lib/mui/line_renderer.rb +52 -1
  35. data/lib/mui/mode_manager.rb +3 -2
  36. data/lib/mui/screen.rb +24 -6
  37. data/lib/mui/search_state.rb +61 -28
  38. data/lib/mui/syntax/language_detector.rb +33 -1
  39. data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
  40. data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
  41. data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
  42. data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
  43. data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
  44. data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
  45. data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
  46. data/lib/mui/terminal_adapter/curses.rb +13 -11
  47. data/lib/mui/version.rb +1 -1
  48. data/lib/mui/window.rb +83 -40
  49. data/lib/mui/window_manager.rb +7 -0
  50. data/lib/mui/wrap_cache.rb +40 -0
  51. data/lib/mui/wrap_helper.rb +84 -0
  52. data/lib/mui.rb +15 -0
  53. metadata +26 -3
data/lib/mui/window.rb CHANGED
@@ -2,7 +2,7 @@
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
+ attr_accessor :x, :y, :width, :height, :cursor_row, :cursor_col, :scroll_row
6
6
  attr_reader :buffer
7
7
 
8
8
  def initialize(buffer, x: 0, y: 0, width: 80, height: 24, color_scheme: nil)
@@ -14,8 +14,8 @@ module Mui
14
14
  @cursor_row = 0
15
15
  @cursor_col = 0
16
16
  @scroll_row = 0
17
- @scroll_col = 0
18
17
  @color_scheme = color_scheme
18
+ @wrap_cache = WrapCache.new
19
19
  @syntax_highlighter = Highlighters::SyntaxHighlighter.new(color_scheme, buffer:)
20
20
  @line_renderer = create_line_renderer
21
21
  @status_line_renderer = StatusLineRenderer.new(buffer, self, color_scheme)
@@ -26,7 +26,7 @@ module Mui
26
26
  @cursor_row = 0
27
27
  @cursor_col = 0
28
28
  @scroll_row = 0
29
- @scroll_col = 0
29
+ @wrap_cache.clear
30
30
  @syntax_highlighter.buffer = new_buffer
31
31
  @line_renderer = create_line_renderer
32
32
  @status_line_renderer = StatusLineRenderer.new(new_buffer, self, @color_scheme)
@@ -41,47 +41,72 @@ module Mui
41
41
  end
42
42
 
43
43
  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
49
- end
44
+ # Calculate screen row of cursor considering line wrapping
45
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
46
+
47
+ # Scroll up if cursor is above visible area
48
+ @scroll_row -= 1 while @cursor_row < @scroll_row
50
49
 
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
50
+ # Scroll down if cursor is below visible area
51
+ while cursor_screen_row >= visible_height
52
+ @scroll_row += 1
53
+ cursor_screen_row = screen_rows_from_scroll_to_cursor
56
54
  end
57
55
  end
58
56
 
59
57
  def render(screen, selection: nil, search_state: nil)
60
58
  options = build_render_options(selection, search_state)
59
+ screen_row = 0
60
+ logical_row = @scroll_row
61
+
62
+ while screen_row < visible_height && logical_row < @buffer.line_count
63
+ line = @buffer.line(logical_row)
64
+ wrapped_lines = WrapHelper.wrap_line(line, visible_width, cache: @wrap_cache)
65
+
66
+ wrapped_lines.each do |wrap_info|
67
+ break if screen_row >= visible_height
68
+
69
+ render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
70
+ screen_row += 1
71
+ end
72
+
73
+ logical_row += 1
74
+ end
61
75
 
62
- visible_height.times do |i|
63
- row = @scroll_row + i
64
- render_line(screen, row, i, options)
76
+ # Clear remaining lines
77
+ while screen_row < visible_height
78
+ clear_line(screen, screen_row)
79
+ screen_row += 1
65
80
  end
66
81
 
67
82
  @status_line_renderer.render(screen, @y + visible_height)
68
83
  end
69
84
 
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)
85
+ def render_wrapped_segment(screen, logical_row, wrap_info, screen_row, options)
86
+ wrap_options = options.merge(logical_row:)
87
+ @line_renderer.render_wrapped_line(screen, @y + screen_row, @x, wrap_info, wrap_options)
88
+
89
+ # Fill remaining width with spaces if line is shorter
90
+ text_width = UnicodeWidth.string_width(wrap_info[:text])
91
+ return unless text_width < visible_width
92
+
93
+ remaining_width = visible_width - text_width
94
+ fill_text = " " * remaining_width
95
+ if @color_scheme && @color_scheme[:normal]
96
+ screen.put_with_style(@y + screen_row, @x + text_width, fill_text, @color_scheme[:normal])
97
+ else
98
+ screen.put(@y + screen_row, @x + text_width, fill_text)
99
+ end
74
100
  end
75
101
 
76
102
  def screen_cursor_x
77
103
  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)
104
+ _, screen_col = WrapHelper.logical_to_screen(line, @cursor_col, visible_width, cache: @wrap_cache)
105
+ @x + screen_col
81
106
  end
82
107
 
83
108
  def screen_cursor_y
84
- @y + @cursor_row - @scroll_row
109
+ @y + screen_rows_from_scroll_to_cursor
85
110
  end
86
111
 
87
112
  # カーソル移動
@@ -115,6 +140,38 @@ module Mui
115
140
 
116
141
  private
117
142
 
143
+ def clear_line(screen, screen_row)
144
+ empty_line = " " * visible_width
145
+ if @color_scheme && @color_scheme[:normal]
146
+ screen.put_with_style(@y + screen_row, @x, empty_line, @color_scheme[:normal])
147
+ else
148
+ screen.put(@y + screen_row, @x, empty_line)
149
+ end
150
+ end
151
+
152
+ # Calculates screen rows from scroll_row to cursor position
153
+ def screen_rows_from_scroll_to_cursor
154
+ screen_rows = 0
155
+
156
+ # Add screen lines for rows between scroll_row and cursor_row
157
+ (@scroll_row...@cursor_row).each do |row|
158
+ line = @buffer.line(row) || ""
159
+ screen_rows += WrapHelper.screen_line_count(line, visible_width, cache: @wrap_cache)
160
+ end
161
+
162
+ # Add the row offset within the cursor's line
163
+ cursor_line = @buffer.line(@cursor_row) || ""
164
+ row_offset, = WrapHelper.logical_to_screen(cursor_line, @cursor_col, visible_width, cache: @wrap_cache)
165
+ screen_rows + row_offset
166
+ end
167
+
168
+ # Clear wrap cache when window dimensions change
169
+ def resize(new_width, new_height)
170
+ @width = new_width
171
+ @height = new_height
172
+ @wrap_cache.clear
173
+ end
174
+
118
175
  def create_line_renderer
119
176
  renderer = LineRenderer.new(@color_scheme)
120
177
  renderer.add_highlighter(@syntax_highlighter)
@@ -129,22 +186,8 @@ module Mui
129
186
  renderer
130
187
  end
131
188
 
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
189
  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
190
+ { selection:, search_state:, buffer: @buffer }
148
191
  end
149
192
 
150
193
  def max_cursor_col
@@ -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.3.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'