mui 0.1.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +163 -0
- data/CHANGELOG.md +448 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +301 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +149 -0
- data/exe/mui +1 -2
- data/lib/mui/autocmd.rb +66 -0
- data/lib/mui/buffer.rb +275 -0
- data/lib/mui/buffer_word_cache.rb +131 -0
- data/lib/mui/buffer_word_completer.rb +77 -0
- data/lib/mui/color_manager.rb +136 -0
- data/lib/mui/color_scheme.rb +63 -0
- data/lib/mui/command_completer.rb +30 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +167 -0
- data/lib/mui/command_registry.rb +44 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +58 -0
- data/lib/mui/editor.rb +395 -0
- data/lib/mui/error.rb +29 -0
- data/lib/mui/file_completer.rb +51 -0
- data/lib/mui/floating_window.rb +161 -0
- data/lib/mui/handler_result.rb +107 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +27 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +107 -0
- data/lib/mui/input.rb +17 -0
- data/lib/mui/insert_completion_renderer.rb +92 -0
- data/lib/mui/insert_completion_state.rb +77 -0
- data/lib/mui/job.rb +81 -0
- data/lib/mui/job_manager.rb +113 -0
- data/lib/mui/key_code.rb +30 -0
- data/lib/mui/key_handler/base.rb +187 -0
- data/lib/mui/key_handler/command_mode.rb +511 -0
- data/lib/mui/key_handler/insert_mode.rb +323 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +552 -0
- data/lib/mui/key_handler/operators/base_operator.rb +134 -0
- data/lib/mui/key_handler/operators/change_operator.rb +179 -0
- data/lib/mui/key_handler/operators/delete_operator.rb +176 -0
- data/lib/mui/key_handler/operators/paste_operator.rb +119 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +191 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +402 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -0
- data/lib/mui/key_notation_parser.rb +152 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/layout/calculator.rb +15 -0
- data/lib/mui/layout/leaf_node.rb +33 -0
- data/lib/mui/layout/node.rb +29 -0
- data/lib/mui/layout/split_node.rb +132 -0
- data/lib/mui/line_renderer.rb +173 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +186 -0
- data/lib/mui/motion.rb +139 -0
- data/lib/mui/plugin.rb +35 -0
- data/lib/mui/plugin_manager.rb +106 -0
- data/lib/mui/register.rb +110 -0
- data/lib/mui/screen.rb +103 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +121 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +106 -0
- data/lib/mui/syntax/lexer_base.rb +106 -0
- data/lib/mui/syntax/lexers/c_lexer.rb +127 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +205 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +197 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +114 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +148 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +203 -0
- data/lib/mui/syntax/token.rb +42 -0
- data/lib/mui/syntax/token_cache.rb +91 -0
- data/lib/mui/tab_bar_renderer.rb +87 -0
- data/lib/mui/tab_manager.rb +96 -0
- data/lib/mui/tab_page.rb +35 -0
- data/lib/mui/terminal_adapter/base.rb +92 -0
- data/lib/mui/terminal_adapter/curses.rb +164 -0
- data/lib/mui/terminal_adapter.rb +4 -0
- data/lib/mui/themes/default.rb +315 -0
- data/lib/mui/undo_manager.rb +83 -0
- data/lib/mui/undoable_action.rb +175 -0
- data/lib/mui/unicode_width.rb +100 -0
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +201 -0
- data/lib/mui/window_manager.rb +256 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +171 -2
- metadata +123 -5
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Base class for undoable actions
|
|
5
|
+
class UndoableAction
|
|
6
|
+
def execute(buffer); end
|
|
7
|
+
def undo(buffer); end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Insert a single character
|
|
11
|
+
class InsertCharAction < UndoableAction
|
|
12
|
+
def initialize(row, col, char)
|
|
13
|
+
super()
|
|
14
|
+
@row = row
|
|
15
|
+
@col = col
|
|
16
|
+
@char = char
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute(buffer)
|
|
20
|
+
buffer.insert_char_without_record(@row, @col, @char)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def undo(buffer)
|
|
24
|
+
buffer.delete_char_without_record(@row, @col)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Delete a single character
|
|
29
|
+
class DeleteCharAction < UndoableAction
|
|
30
|
+
def initialize(row, col, char)
|
|
31
|
+
super()
|
|
32
|
+
@row = row
|
|
33
|
+
@col = col
|
|
34
|
+
@char = char
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute(buffer)
|
|
38
|
+
buffer.delete_char_without_record(@row, @col)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def undo(buffer)
|
|
42
|
+
buffer.insert_char_without_record(@row, @col, @char)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Insert a line
|
|
47
|
+
class InsertLineAction < UndoableAction
|
|
48
|
+
def initialize(row, text)
|
|
49
|
+
super()
|
|
50
|
+
@row = row
|
|
51
|
+
@text = text
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def execute(buffer)
|
|
55
|
+
buffer.insert_line_without_record(@row, @text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def undo(buffer)
|
|
59
|
+
buffer.delete_line_without_record(@row)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Delete a line
|
|
64
|
+
class DeleteLineAction < UndoableAction
|
|
65
|
+
def initialize(row, text)
|
|
66
|
+
super()
|
|
67
|
+
@row = row
|
|
68
|
+
@text = text
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def execute(buffer)
|
|
72
|
+
buffer.delete_line_without_record(@row)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def undo(buffer)
|
|
76
|
+
buffer.insert_line_without_record(@row, @text)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Split a line (Enter key)
|
|
81
|
+
class SplitLineAction < UndoableAction
|
|
82
|
+
def initialize(row, col)
|
|
83
|
+
super()
|
|
84
|
+
@row = row
|
|
85
|
+
@col = col
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def execute(buffer)
|
|
89
|
+
buffer.split_line_without_record(@row, @col)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def undo(buffer)
|
|
93
|
+
buffer.join_lines_without_record(@row)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Join lines (Backspace at line start)
|
|
98
|
+
class JoinLinesAction < UndoableAction
|
|
99
|
+
def initialize(row, col)
|
|
100
|
+
super()
|
|
101
|
+
@row = row
|
|
102
|
+
@col = col
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def execute(buffer)
|
|
106
|
+
buffer.join_lines_without_record(@row)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def undo(buffer)
|
|
110
|
+
buffer.split_line_without_record(@row, @col)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Delete a range of text
|
|
115
|
+
class DeleteRangeAction < UndoableAction
|
|
116
|
+
def initialize(start_row, start_col, end_row, end_col, deleted_lines)
|
|
117
|
+
super()
|
|
118
|
+
@start_row = start_row
|
|
119
|
+
@start_col = start_col
|
|
120
|
+
@end_row = end_row
|
|
121
|
+
@end_col = end_col
|
|
122
|
+
@deleted_lines = deleted_lines
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def execute(buffer)
|
|
126
|
+
buffer.delete_range_without_record(@start_row, @start_col, @end_row, @end_col)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def undo(buffer)
|
|
130
|
+
buffer.restore_range(@start_row, @start_col, @deleted_lines)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Replace line content (for cc command)
|
|
135
|
+
class ReplaceLineAction < UndoableAction
|
|
136
|
+
def initialize(row, old_text, new_text)
|
|
137
|
+
super()
|
|
138
|
+
@row = row
|
|
139
|
+
@old_text = old_text
|
|
140
|
+
@new_text = new_text
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def execute(buffer)
|
|
144
|
+
buffer.replace_line_without_record(@row, @new_text)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def undo(buffer)
|
|
148
|
+
buffer.replace_line_without_record(@row, @old_text)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Group multiple actions into one undo unit
|
|
153
|
+
class GroupAction < UndoableAction
|
|
154
|
+
def initialize(actions)
|
|
155
|
+
super()
|
|
156
|
+
@actions = actions
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def execute(buffer)
|
|
160
|
+
@actions.each { |action| action.execute(buffer) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def undo(buffer)
|
|
164
|
+
@actions.reverse_each { |action| action.undo(buffer) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def empty?
|
|
168
|
+
@actions.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def size
|
|
172
|
+
@actions.size
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Utility module for calculating display width of Unicode characters
|
|
5
|
+
# CJK characters and some other characters are "wide" (2 cells)
|
|
6
|
+
module UnicodeWidth
|
|
7
|
+
class << self
|
|
8
|
+
# Returns the display width of a single character
|
|
9
|
+
def char_width(char)
|
|
10
|
+
return 0 if char.nil? || char.empty?
|
|
11
|
+
|
|
12
|
+
ord = char.ord
|
|
13
|
+
|
|
14
|
+
# Control characters
|
|
15
|
+
return 0 if ord < 32
|
|
16
|
+
|
|
17
|
+
# ASCII printable characters
|
|
18
|
+
return 1 if ord < 127
|
|
19
|
+
|
|
20
|
+
# Non-printable
|
|
21
|
+
return 0 if ord == 127
|
|
22
|
+
|
|
23
|
+
# Wide characters (East Asian Wide and Fullwidth)
|
|
24
|
+
return 2 if wide_char?(ord)
|
|
25
|
+
|
|
26
|
+
# Default to 1 for other characters
|
|
27
|
+
1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the display width of a string
|
|
31
|
+
def string_width(str)
|
|
32
|
+
return 0 if str.nil?
|
|
33
|
+
|
|
34
|
+
str.chars.sum { |c| char_width(c) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the display width of a substring from index 0 to col (exclusive)
|
|
38
|
+
def width_to_col(str, col)
|
|
39
|
+
return 0 if str.nil? || col <= 0
|
|
40
|
+
|
|
41
|
+
str.chars.take(col).sum { |c| char_width(c) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the character index for a given display width position
|
|
45
|
+
def col_at_width(str, target_width)
|
|
46
|
+
return 0 if str.nil? || target_width <= 0
|
|
47
|
+
|
|
48
|
+
current_width = 0
|
|
49
|
+
str.chars.each_with_index do |char, index|
|
|
50
|
+
return index if current_width >= target_width
|
|
51
|
+
|
|
52
|
+
current_width += char_width(char)
|
|
53
|
+
end
|
|
54
|
+
str.length
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def wide_char?(ord)
|
|
60
|
+
# CJK ranges (simplified, covers most common cases)
|
|
61
|
+
# Full implementation would use Unicode East Asian Width property
|
|
62
|
+
|
|
63
|
+
# Hangul Jamo
|
|
64
|
+
return true if ord.between?(0x1100, 0x115F)
|
|
65
|
+
|
|
66
|
+
# CJK Radicals Supplement to Enclosed CJK Letters
|
|
67
|
+
return true if ord.between?(0x2E80, 0x4DBF)
|
|
68
|
+
|
|
69
|
+
# CJK Unified Ideographs
|
|
70
|
+
return true if ord.between?(0x4E00, 0x9FFF)
|
|
71
|
+
|
|
72
|
+
# Hangul Syllables
|
|
73
|
+
return true if ord.between?(0xAC00, 0xD7AF)
|
|
74
|
+
|
|
75
|
+
# CJK Compatibility Ideographs
|
|
76
|
+
return true if ord.between?(0xF900, 0xFAFF)
|
|
77
|
+
|
|
78
|
+
# Fullwidth Forms
|
|
79
|
+
return true if ord.between?(0xFF00, 0xFF60)
|
|
80
|
+
|
|
81
|
+
# CJK Unified Ideographs Extension B-F
|
|
82
|
+
return true if ord.between?(0x20000, 0x2FA1F)
|
|
83
|
+
|
|
84
|
+
# Halfwidth Katakana (narrow, actually 1)
|
|
85
|
+
return false if ord.between?(0xFF61, 0xFFDC)
|
|
86
|
+
|
|
87
|
+
# Japanese Hiragana
|
|
88
|
+
return true if ord.between?(0x3040, 0x309F)
|
|
89
|
+
|
|
90
|
+
# Japanese Katakana
|
|
91
|
+
return true if ord.between?(0x30A0, 0x30FF)
|
|
92
|
+
|
|
93
|
+
# CJK Symbols and Punctuation
|
|
94
|
+
return true if ord.between?(0x3000, 0x303F)
|
|
95
|
+
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/mui/version.rb
CHANGED
data/lib/mui/window.rb
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
class Window
|
|
5
|
+
attr_accessor :x, :y, :width, :height, :cursor_row, :cursor_col, :scroll_row
|
|
6
|
+
attr_reader :buffer
|
|
7
|
+
|
|
8
|
+
def initialize(buffer, x: 0, y: 0, width: 80, height: 24, color_scheme: nil)
|
|
9
|
+
@buffer = buffer
|
|
10
|
+
@x = x
|
|
11
|
+
@y = y
|
|
12
|
+
@width = width
|
|
13
|
+
@height = height
|
|
14
|
+
@cursor_row = 0
|
|
15
|
+
@cursor_col = 0
|
|
16
|
+
@scroll_row = 0
|
|
17
|
+
@color_scheme = color_scheme
|
|
18
|
+
@wrap_cache = WrapCache.new
|
|
19
|
+
@syntax_highlighter = Highlighters::SyntaxHighlighter.new(color_scheme, buffer:)
|
|
20
|
+
@line_renderer = create_line_renderer
|
|
21
|
+
@status_line_renderer = StatusLineRenderer.new(buffer, self, color_scheme)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def buffer=(new_buffer)
|
|
25
|
+
@buffer = new_buffer
|
|
26
|
+
@cursor_row = 0
|
|
27
|
+
@cursor_col = 0
|
|
28
|
+
@scroll_row = 0
|
|
29
|
+
@wrap_cache.clear
|
|
30
|
+
@syntax_highlighter.buffer = new_buffer
|
|
31
|
+
@line_renderer = create_line_renderer
|
|
32
|
+
@status_line_renderer = StatusLineRenderer.new(new_buffer, self, @color_scheme)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def visible_height
|
|
36
|
+
@height - 1 # Status line only (command line is shared by all windows)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visible_width
|
|
40
|
+
@width
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ensure_cursor_visible
|
|
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
|
|
49
|
+
|
|
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
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render(screen, selection: nil, search_state: nil)
|
|
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
|
|
75
|
+
|
|
76
|
+
# Clear remaining lines
|
|
77
|
+
while screen_row < visible_height
|
|
78
|
+
clear_line(screen, screen_row)
|
|
79
|
+
screen_row += 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@status_line_renderer.render(screen, @y + visible_height)
|
|
83
|
+
end
|
|
84
|
+
|
|
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
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def screen_cursor_x
|
|
103
|
+
line = @buffer.line(@cursor_row) || ""
|
|
104
|
+
_, screen_col = WrapHelper.logical_to_screen(line, @cursor_col, visible_width, cache: @wrap_cache)
|
|
105
|
+
@x + screen_col
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def screen_cursor_y
|
|
109
|
+
@y + screen_rows_from_scroll_to_cursor
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# カーソル移動
|
|
113
|
+
def move_left
|
|
114
|
+
@cursor_col -= 1 if @cursor_col.positive?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def move_right
|
|
118
|
+
@cursor_col += 1 if @cursor_col < max_cursor_col
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def move_up
|
|
122
|
+
@cursor_row -= 1 if @cursor_row.positive?
|
|
123
|
+
clamp_cursor_col
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def move_down
|
|
127
|
+
@cursor_row += 1 if @cursor_row < @buffer.line_count - 1
|
|
128
|
+
clamp_cursor_col
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def clamp_cursor_to_line(buffer)
|
|
132
|
+
max_col = [buffer.line(@cursor_row).length - 1, 0].max
|
|
133
|
+
@cursor_col = max_col if @cursor_col > max_col
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Refresh highlighters (call when custom highlighters change)
|
|
137
|
+
def refresh_highlighters
|
|
138
|
+
@line_renderer = create_line_renderer
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
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
|
+
|
|
175
|
+
def create_line_renderer
|
|
176
|
+
renderer = LineRenderer.new(@color_scheme)
|
|
177
|
+
renderer.add_highlighter(@syntax_highlighter)
|
|
178
|
+
renderer.add_highlighter(Highlighters::SelectionHighlighter.new(@color_scheme))
|
|
179
|
+
renderer.add_highlighter(Highlighters::SearchHighlighter.new(@color_scheme))
|
|
180
|
+
|
|
181
|
+
# Add buffer-specific custom highlighters
|
|
182
|
+
@buffer.custom_highlighters(@color_scheme).each do |highlighter|
|
|
183
|
+
renderer.add_highlighter(highlighter)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
renderer
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_render_options(selection, search_state)
|
|
190
|
+
{ selection:, search_state:, buffer: @buffer }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def max_cursor_col
|
|
194
|
+
[@buffer.line(@cursor_row).length - 1, 0].max
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def clamp_cursor_col
|
|
198
|
+
@cursor_col = max_cursor_col if @cursor_col > max_cursor_col
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|