mui 0.1.0 → 0.2.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 +158 -0
- data/CHANGELOG.md +349 -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 +21 -0
- data/lib/mui/command_context.rb +90 -0
- data/lib/mui/command_line.rb +137 -0
- data/lib/mui/command_registry.rb +25 -0
- data/lib/mui/completion_renderer.rb +84 -0
- data/lib/mui/completion_state.rb +58 -0
- data/lib/mui/config.rb +56 -0
- data/lib/mui/editor.rb +319 -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 +101 -0
- data/lib/mui/highlight.rb +22 -0
- data/lib/mui/highlighters/base.rb +23 -0
- data/lib/mui/highlighters/search_highlighter.rb +26 -0
- data/lib/mui/highlighters/selection_highlighter.rb +48 -0
- data/lib/mui/highlighters/syntax_highlighter.rb +105 -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 +100 -0
- data/lib/mui/key_handler/command_mode.rb +443 -0
- data/lib/mui/key_handler/insert_mode.rb +354 -0
- data/lib/mui/key_handler/motions/motion_handler.rb +56 -0
- data/lib/mui/key_handler/normal_mode.rb +579 -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 +113 -0
- data/lib/mui/key_handler/operators/yank_operator.rb +127 -0
- data/lib/mui/key_handler/search_mode.rb +188 -0
- data/lib/mui/key_handler/visual_line_mode.rb +20 -0
- data/lib/mui/key_handler/visual_mode.rb +397 -0
- data/lib/mui/key_handler/window_command.rb +112 -0
- data/lib/mui/key_handler.rb +16 -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 +122 -0
- data/lib/mui/mode.rb +13 -0
- data/lib/mui/mode_manager.rb +185 -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 +85 -0
- data/lib/mui/search_completer.rb +50 -0
- data/lib/mui/search_input.rb +40 -0
- data/lib/mui/search_state.rb +88 -0
- data/lib/mui/selection.rb +55 -0
- data/lib/mui/status_line_renderer.rb +40 -0
- data/lib/mui/syntax/language_detector.rb +74 -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/ruby_lexer.rb +114 -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 +162 -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 +158 -0
- data/lib/mui/window_manager.rb +249 -0
- data/lib/mui.rb +156 -2
- metadata +98 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "key_handler/motions/motion_handler"
|
|
4
|
+
require_relative "key_handler/operators/base_operator"
|
|
5
|
+
require_relative "key_handler/operators/delete_operator"
|
|
6
|
+
require_relative "key_handler/operators/change_operator"
|
|
7
|
+
require_relative "key_handler/operators/yank_operator"
|
|
8
|
+
require_relative "key_handler/operators/paste_operator"
|
|
9
|
+
require_relative "key_handler/base"
|
|
10
|
+
require_relative "key_handler/normal_mode"
|
|
11
|
+
require_relative "key_handler/insert_mode"
|
|
12
|
+
require_relative "key_handler/command_mode"
|
|
13
|
+
require_relative "key_handler/visual_mode"
|
|
14
|
+
require_relative "key_handler/visual_line_mode"
|
|
15
|
+
require_relative "key_handler/search_mode"
|
|
16
|
+
require_relative "key_handler/window_command"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Layout
|
|
5
|
+
class LeafNode < Node
|
|
6
|
+
attr_accessor :window
|
|
7
|
+
|
|
8
|
+
def initialize(window)
|
|
9
|
+
super()
|
|
10
|
+
@window = window
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def leaf?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def windows
|
|
18
|
+
[@window]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_window_node(target_window)
|
|
22
|
+
@window == target_window ? self : nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def apply_geometry
|
|
26
|
+
@window.x = @x
|
|
27
|
+
@window.y = @y
|
|
28
|
+
@window.width = @width
|
|
29
|
+
@window.height = @height
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Layout
|
|
5
|
+
class Node
|
|
6
|
+
attr_accessor :parent, :x, :y, :width, :height
|
|
7
|
+
|
|
8
|
+
def leaf?
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def split?
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def windows
|
|
17
|
+
raise Mui::MethodNotOverriddenError, :windows
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_window_node(_window)
|
|
21
|
+
raise Mui::MethodNotOverriddenError, :find_window_node
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply_geometry
|
|
25
|
+
raise Mui::MethodNotOverriddenError, :apply_geometry
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Layout
|
|
5
|
+
class SplitNode < Node
|
|
6
|
+
SEPARATOR_SIZE = 1
|
|
7
|
+
|
|
8
|
+
attr_accessor :direction, :children, :ratio
|
|
9
|
+
|
|
10
|
+
def initialize(direction:, children: [], ratio: 0.5)
|
|
11
|
+
super()
|
|
12
|
+
@direction = direction
|
|
13
|
+
@children = children
|
|
14
|
+
@ratio = ratio
|
|
15
|
+
children.each { |c| c.parent = self }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def separators
|
|
19
|
+
result = []
|
|
20
|
+
collect_separators(result)
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def split?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def windows
|
|
29
|
+
@children.flat_map(&:windows)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def find_window_node(target_window)
|
|
33
|
+
@children.each do |child|
|
|
34
|
+
result = child.find_window_node(target_window)
|
|
35
|
+
return result if result
|
|
36
|
+
end
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_geometry
|
|
41
|
+
return if @children.empty?
|
|
42
|
+
|
|
43
|
+
if @children.size == 1
|
|
44
|
+
apply_single_child
|
|
45
|
+
else
|
|
46
|
+
apply_split_children
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@children.each(&:apply_geometry)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def replace_child(old_child, new_child)
|
|
53
|
+
index = @children.index(old_child)
|
|
54
|
+
return unless index
|
|
55
|
+
|
|
56
|
+
@children[index] = new_child
|
|
57
|
+
new_child.parent = self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def remove_child(child)
|
|
61
|
+
@children.delete(child)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def apply_single_child
|
|
67
|
+
child = @children.first
|
|
68
|
+
child.x = @x
|
|
69
|
+
child.y = @y
|
|
70
|
+
child.width = @width
|
|
71
|
+
child.height = @height
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apply_split_children
|
|
75
|
+
case @direction
|
|
76
|
+
when :horizontal
|
|
77
|
+
apply_horizontal_split
|
|
78
|
+
when :vertical
|
|
79
|
+
apply_vertical_split
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def apply_horizontal_split
|
|
84
|
+
available_height = @height - SEPARATOR_SIZE
|
|
85
|
+
first_height = (available_height * @ratio).to_i
|
|
86
|
+
second_height = available_height - first_height
|
|
87
|
+
|
|
88
|
+
@children[0].x = @x
|
|
89
|
+
@children[0].y = @y
|
|
90
|
+
@children[0].width = @width
|
|
91
|
+
@children[0].height = first_height
|
|
92
|
+
|
|
93
|
+
@children[1].x = @x
|
|
94
|
+
@children[1].y = @y + first_height + SEPARATOR_SIZE
|
|
95
|
+
@children[1].width = @width
|
|
96
|
+
@children[1].height = second_height
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def apply_vertical_split
|
|
100
|
+
available_width = @width - SEPARATOR_SIZE
|
|
101
|
+
first_width = (available_width * @ratio).to_i
|
|
102
|
+
second_width = available_width - first_width
|
|
103
|
+
|
|
104
|
+
@children[0].x = @x
|
|
105
|
+
@children[0].y = @y
|
|
106
|
+
@children[0].width = first_width
|
|
107
|
+
@children[0].height = @height
|
|
108
|
+
|
|
109
|
+
@children[1].x = @x + first_width + SEPARATOR_SIZE
|
|
110
|
+
@children[1].y = @y
|
|
111
|
+
@children[1].width = second_width
|
|
112
|
+
@children[1].height = @height
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def collect_separators(result)
|
|
116
|
+
return if @children.size < 2
|
|
117
|
+
|
|
118
|
+
if @direction == :horizontal
|
|
119
|
+
separator_y = @children[0].y + @children[0].height
|
|
120
|
+
result << { type: :horizontal, x: @x, y: separator_y, length: @width }
|
|
121
|
+
else
|
|
122
|
+
separator_x = @children[0].x + @children[0].width
|
|
123
|
+
result << { type: :vertical, x: separator_x, y: @y, length: @height }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@children.each do |child|
|
|
127
|
+
child.send(:collect_separators, result) if child.respond_to?(:collect_separators, true)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
class LineRenderer
|
|
5
|
+
def initialize(color_scheme)
|
|
6
|
+
@color_scheme = color_scheme
|
|
7
|
+
@highlighters = []
|
|
8
|
+
@resolved_styles = {} # Cache for resolved styles
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_highlighter(highlighter)
|
|
12
|
+
@highlighters << highlighter
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(screen, line, row, x, y, options = {})
|
|
16
|
+
highlights = collect_highlights(row, line, options)
|
|
17
|
+
render_with_highlights(screen, line, x, y, highlights)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def collect_highlights(row, line, options)
|
|
23
|
+
@highlighters
|
|
24
|
+
.flat_map { |h| h.highlights_for(row, line, options) }
|
|
25
|
+
.sort
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_with_highlights(screen, line, x, y, highlights)
|
|
29
|
+
if highlights.empty?
|
|
30
|
+
put_text(screen, y, x, line, :normal)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
segments = build_segments(line, highlights)
|
|
35
|
+
current_x = x
|
|
36
|
+
|
|
37
|
+
segments.each do |segment|
|
|
38
|
+
put_text(screen, y, current_x, segment[:text], segment[:style])
|
|
39
|
+
current_x += segment[:text].length
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_segments(line, highlights)
|
|
44
|
+
segments = []
|
|
45
|
+
current_pos = 0
|
|
46
|
+
active_highlights = []
|
|
47
|
+
|
|
48
|
+
events = build_events(highlights, line.length)
|
|
49
|
+
|
|
50
|
+
events.each do |event|
|
|
51
|
+
if event[:pos] > current_pos && current_pos < line.length
|
|
52
|
+
end_pos = [event[:pos], line.length].min
|
|
53
|
+
style = active_highlights.max_by(&:priority)&.style || :normal
|
|
54
|
+
text = line[current_pos...end_pos]
|
|
55
|
+
segments << { text:, style: } unless text.empty?
|
|
56
|
+
current_pos = end_pos
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
case event[:type]
|
|
60
|
+
when :start
|
|
61
|
+
active_highlights << event[:highlight]
|
|
62
|
+
when :end
|
|
63
|
+
active_highlights.delete(event[:highlight])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if current_pos < line.length
|
|
68
|
+
style = active_highlights.max_by(&:priority)&.style || :normal
|
|
69
|
+
segments << { text: line[current_pos..], style: }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
segments
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_events(highlights, line_length)
|
|
76
|
+
return [] if highlights.empty?
|
|
77
|
+
|
|
78
|
+
events = []
|
|
79
|
+
highlights.each do |h|
|
|
80
|
+
start_col = [h.start_col, 0].max
|
|
81
|
+
end_col = [h.end_col, line_length - 1].min
|
|
82
|
+
next if start_col > end_col
|
|
83
|
+
|
|
84
|
+
events << [start_col, 1, h] # 1 = start (sorted after end at same position)
|
|
85
|
+
events << [end_col + 1, 0, h] # 0 = end
|
|
86
|
+
end
|
|
87
|
+
# Sort by position, then by type (end before start at same position)
|
|
88
|
+
events.sort!
|
|
89
|
+
# Convert back to hash format
|
|
90
|
+
events.map! { |pos, type, h| { pos:, type: type == 1 ? :start : :end, highlight: h } }
|
|
91
|
+
events
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def put_text(screen, y, x, text, style)
|
|
95
|
+
return if text.nil? || text.empty?
|
|
96
|
+
|
|
97
|
+
if @color_scheme && @color_scheme[style]
|
|
98
|
+
resolved_style = resolve_style(style)
|
|
99
|
+
screen.put_with_style(y, x, text, resolved_style)
|
|
100
|
+
else
|
|
101
|
+
screen.put(y, x, text)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def resolve_style(style)
|
|
106
|
+
# Use cached resolved style if available
|
|
107
|
+
return @resolved_styles[style] if @resolved_styles.key?(style)
|
|
108
|
+
|
|
109
|
+
style_hash = @color_scheme[style]
|
|
110
|
+
resolved = if style_hash[:bg]
|
|
111
|
+
style_hash
|
|
112
|
+
else
|
|
113
|
+
# Inherit background from :normal if not specified
|
|
114
|
+
normal_style = @color_scheme[:normal]
|
|
115
|
+
normal_style ? style_hash.merge(bg: normal_style[:bg]) : style_hash
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
@resolved_styles[style] = resolved
|
|
119
|
+
resolved
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/mui/mode.rb
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
# Manages editor mode state and transitions
|
|
5
|
+
class ModeManager
|
|
6
|
+
attr_reader :mode, :selection, :register, :undo_manager, :search_state, :search_input, :editor,
|
|
7
|
+
:last_visual_selection
|
|
8
|
+
|
|
9
|
+
def initialize(window:, buffer:, command_line:, undo_manager: nil, editor: nil, register: nil)
|
|
10
|
+
@tab_manager = window.is_a?(TabManager) ? window : nil
|
|
11
|
+
@window_manager = window.is_a?(WindowManager) ? window : nil
|
|
12
|
+
@window = !@tab_manager && !@window_manager ? window : nil
|
|
13
|
+
@buffer = buffer
|
|
14
|
+
@command_line = command_line
|
|
15
|
+
@register = register || Mui.register
|
|
16
|
+
@undo_manager = undo_manager
|
|
17
|
+
@editor = editor
|
|
18
|
+
@search_state = SearchState.new
|
|
19
|
+
@search_input = SearchInput.new
|
|
20
|
+
@mode = Mode::NORMAL
|
|
21
|
+
@selection = nil
|
|
22
|
+
@visual_handler = nil
|
|
23
|
+
@last_visual_selection = nil
|
|
24
|
+
|
|
25
|
+
initialize_key_handlers
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def window_manager
|
|
29
|
+
@tab_manager&.window_manager || @window_manager
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current_handler
|
|
33
|
+
if visual_mode?
|
|
34
|
+
@visual_handler || @key_handlers[Mode::NORMAL]
|
|
35
|
+
else
|
|
36
|
+
@key_handlers[@mode]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def transition(result)
|
|
41
|
+
return unless result.mode
|
|
42
|
+
|
|
43
|
+
clear_visual_mode if result.clear_selection?
|
|
44
|
+
|
|
45
|
+
case result.mode
|
|
46
|
+
when Mode::VISUAL, Mode::VISUAL_LINE
|
|
47
|
+
handle_visual_transition(result)
|
|
48
|
+
when Mode::INSERT
|
|
49
|
+
handle_insert_transition(result)
|
|
50
|
+
when Mode::SEARCH_FORWARD
|
|
51
|
+
handle_search_forward_transition
|
|
52
|
+
when Mode::SEARCH_BACKWARD
|
|
53
|
+
handle_search_backward_transition
|
|
54
|
+
else
|
|
55
|
+
@mode = result.mode
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def visual_mode?
|
|
60
|
+
@mode == Mode::VISUAL || @mode == Mode::VISUAL_LINE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def active_window
|
|
64
|
+
@tab_manager&.active_window || @window_manager&.active_window || @window
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
alias window active_window
|
|
68
|
+
|
|
69
|
+
def restore_visual_selection
|
|
70
|
+
return unless @last_visual_selection
|
|
71
|
+
|
|
72
|
+
line_mode = @last_visual_selection[:line_mode]
|
|
73
|
+
@mode = line_mode ? Mode::VISUAL_LINE : Mode::VISUAL
|
|
74
|
+
@selection = Selection.new(
|
|
75
|
+
@last_visual_selection[:start_row],
|
|
76
|
+
@last_visual_selection[:start_col],
|
|
77
|
+
line_mode:
|
|
78
|
+
)
|
|
79
|
+
@selection.update_end(
|
|
80
|
+
@last_visual_selection[:end_row],
|
|
81
|
+
@last_visual_selection[:end_col]
|
|
82
|
+
)
|
|
83
|
+
@visual_handler = if line_mode
|
|
84
|
+
KeyHandler::VisualLineMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
|
|
85
|
+
else
|
|
86
|
+
KeyHandler::VisualMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Move cursor to end of selection
|
|
90
|
+
active_window.cursor_row = @last_visual_selection[:end_row]
|
|
91
|
+
active_window.cursor_col = @last_visual_selection[:end_col]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def initialize_key_handlers
|
|
97
|
+
@key_handlers = {
|
|
98
|
+
Mode::NORMAL => KeyHandler::NormalMode.new(self, @buffer, @register, undo_manager: @undo_manager, search_state: @search_state),
|
|
99
|
+
# Use group_started: true to prevent begin_group on initialization
|
|
100
|
+
# The handler will be replaced when actually entering Insert mode
|
|
101
|
+
Mode::INSERT => KeyHandler::InsertMode.new(self, @buffer, undo_manager: @undo_manager, group_started: true),
|
|
102
|
+
Mode::COMMAND => KeyHandler::CommandMode.new(self, @buffer, @command_line),
|
|
103
|
+
Mode::SEARCH_FORWARD => KeyHandler::SearchMode.new(self, @buffer, @search_input, @search_state),
|
|
104
|
+
Mode::SEARCH_BACKWARD => KeyHandler::SearchMode.new(self, @buffer, @search_input, @search_state)
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_insert_handler(group_started: false)
|
|
109
|
+
KeyHandler::InsertMode.new(self, @buffer, undo_manager: @undo_manager, group_started:)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def handle_insert_transition(result)
|
|
113
|
+
group_started = result.respond_to?(:group_started?) && result.group_started?
|
|
114
|
+
@key_handlers[Mode::INSERT] = create_insert_handler(group_started:)
|
|
115
|
+
@mode = Mode::INSERT
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_visual_transition(result)
|
|
119
|
+
if result.start_selection?
|
|
120
|
+
start_visual_mode(result.mode, result.line_mode?)
|
|
121
|
+
elsif result.toggle_line_mode?
|
|
122
|
+
toggle_visual_line_mode(result.mode)
|
|
123
|
+
else
|
|
124
|
+
@mode = result.mode
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def start_visual_mode(mode, line_mode)
|
|
129
|
+
@mode = mode
|
|
130
|
+
@visual_handler = create_visual_handler(line_mode:)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def clear_visual_mode
|
|
134
|
+
save_visual_selection if @selection
|
|
135
|
+
@selection = nil
|
|
136
|
+
@visual_handler = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def save_visual_selection
|
|
140
|
+
return unless @selection
|
|
141
|
+
|
|
142
|
+
@last_visual_selection = {
|
|
143
|
+
start_row: @selection.start_row,
|
|
144
|
+
start_col: @selection.start_col,
|
|
145
|
+
end_row: @selection.end_row,
|
|
146
|
+
end_col: @selection.end_col,
|
|
147
|
+
line_mode: @selection.line_mode
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def toggle_visual_line_mode(new_mode)
|
|
152
|
+
return unless @selection
|
|
153
|
+
|
|
154
|
+
new_line_mode = new_mode == Mode::VISUAL_LINE
|
|
155
|
+
@selection = Selection.new(@selection.start_row, @selection.start_col, line_mode: new_line_mode)
|
|
156
|
+
@selection.update_end(active_window.cursor_row, active_window.cursor_col)
|
|
157
|
+
@visual_handler = create_visual_handler(line_mode: new_line_mode)
|
|
158
|
+
@mode = new_mode
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def create_visual_handler(line_mode:)
|
|
162
|
+
@selection = Selection.new(active_window.cursor_row, active_window.cursor_col, line_mode:)
|
|
163
|
+
|
|
164
|
+
if line_mode
|
|
165
|
+
KeyHandler::VisualLineMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
|
|
166
|
+
else
|
|
167
|
+
KeyHandler::VisualMode.new(self, @buffer, @selection, @register, undo_manager: @undo_manager)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def handle_search_forward_transition
|
|
172
|
+
@search_input.clear
|
|
173
|
+
@search_input.set_prompt("/")
|
|
174
|
+
@key_handlers[Mode::SEARCH_FORWARD].start_search
|
|
175
|
+
@mode = Mode::SEARCH_FORWARD
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_search_backward_transition
|
|
179
|
+
@search_input.clear
|
|
180
|
+
@search_input.set_prompt("?")
|
|
181
|
+
@key_handlers[Mode::SEARCH_BACKWARD].start_search
|
|
182
|
+
@mode = Mode::SEARCH_BACKWARD
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
data/lib/mui/motion.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Motion
|
|
5
|
+
WORD_CHARS = /[a-zA-Z0-9_]/
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# Basic movements
|
|
9
|
+
def left(_buffer, row, col)
|
|
10
|
+
col.positive? ? { row:, col: col - 1 } : nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def right(buffer, row, col)
|
|
14
|
+
line = buffer.line(row)
|
|
15
|
+
col < line.size - 1 ? { row:, col: col + 1 } : nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def up(_buffer, row, col)
|
|
19
|
+
row.positive? ? { row: row - 1, col: } : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def down(buffer, row, col)
|
|
23
|
+
row < buffer.line_count - 1 ? { row: row + 1, col: } : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Word movements
|
|
27
|
+
def word_forward(buffer, row, col)
|
|
28
|
+
line = buffer.line(row)
|
|
29
|
+
|
|
30
|
+
# Move past current word
|
|
31
|
+
col += 1 while col < line.size && line[col] =~ WORD_CHARS
|
|
32
|
+
|
|
33
|
+
# Skip whitespace
|
|
34
|
+
col += 1 while col < line.size && line[col] =~ /\s/
|
|
35
|
+
|
|
36
|
+
# If at end of line, move to next line
|
|
37
|
+
if col >= line.size && row < buffer.line_count - 1
|
|
38
|
+
next_line = buffer.line(row + 1)
|
|
39
|
+
# Find first non-whitespace character on next line
|
|
40
|
+
next_col = 0
|
|
41
|
+
next_col += 1 while next_col < next_line.size && next_line[next_col] =~ /\s/
|
|
42
|
+
return { row: row + 1, col: next_col }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
{ row:, col: }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def word_backward(buffer, row, col)
|
|
49
|
+
buffer.line(row)
|
|
50
|
+
|
|
51
|
+
# If at start of line, go to previous line
|
|
52
|
+
if col.zero? && row.positive?
|
|
53
|
+
prev_line = buffer.line(row - 1)
|
|
54
|
+
return word_backward(buffer, row - 1, prev_line.size)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Move back one position to check previous character
|
|
58
|
+
col -= 1 if col.positive?
|
|
59
|
+
line = buffer.line(row)
|
|
60
|
+
|
|
61
|
+
# Skip whitespace
|
|
62
|
+
col -= 1 while col.positive? && line[col] =~ /\s/
|
|
63
|
+
|
|
64
|
+
# Move to start of word
|
|
65
|
+
col -= 1 while col.positive? && line[col - 1] =~ WORD_CHARS
|
|
66
|
+
|
|
67
|
+
{ row:, col: }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def word_end(buffer, row, col)
|
|
71
|
+
line = buffer.line(row)
|
|
72
|
+
|
|
73
|
+
# Move forward one position
|
|
74
|
+
col += 1 if col < line.size
|
|
75
|
+
|
|
76
|
+
# Skip whitespace
|
|
77
|
+
col += 1 while col < line.size && line[col] =~ /\s/
|
|
78
|
+
|
|
79
|
+
# If at end of line, move to next line
|
|
80
|
+
return word_end(buffer, row + 1, 0) if col >= line.size && row < buffer.line_count - 1
|
|
81
|
+
|
|
82
|
+
# Move to end of word
|
|
83
|
+
col += 1 while col < line.size - 1 && line[col + 1] =~ WORD_CHARS
|
|
84
|
+
|
|
85
|
+
{ row:, col: }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Line start/end movements
|
|
89
|
+
def line_start(_buffer, row, _col)
|
|
90
|
+
{ row:, col: 0 }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def first_non_blank(buffer, row, _col)
|
|
94
|
+
line = buffer.line(row)
|
|
95
|
+
new_col = line.index(/\S/) || 0
|
|
96
|
+
{ row:, col: new_col }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def line_end(buffer, row, _col)
|
|
100
|
+
line = buffer.line(row)
|
|
101
|
+
{ row:, col: [line.size - 1, 0].max }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# File start/end movements
|
|
105
|
+
def file_start(_buffer, _row, _col)
|
|
106
|
+
{ row: 0, col: 0 }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def file_end(buffer, _row, _col)
|
|
110
|
+
last_row = buffer.line_count - 1
|
|
111
|
+
{ row: last_row, col: 0 }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Character search (f, F, t, T)
|
|
115
|
+
def find_char_forward(buffer, row, col, char)
|
|
116
|
+
line = buffer.line(row)
|
|
117
|
+
index = line.index(char, col + 1)
|
|
118
|
+
index ? { row:, col: index } : nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def find_char_backward(buffer, row, col, char)
|
|
122
|
+
line = buffer.line(row)
|
|
123
|
+
search_range = line[0...col]
|
|
124
|
+
index = search_range.rindex(char)
|
|
125
|
+
index ? { row:, col: index } : nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def till_char_forward(buffer, row, col, char)
|
|
129
|
+
result = find_char_forward(buffer, row, col, char)
|
|
130
|
+
result ? { row: result[:row], col: result[:col] - 1 } : nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def till_char_backward(buffer, row, col, char)
|
|
134
|
+
result = find_char_backward(buffer, row, col, char)
|
|
135
|
+
result ? { row: result[:row], col: result[:col] + 1 } : nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|