vimamsa 0.1.22 → 0.1.24
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/.dockerignore +32 -0
- data/Dockerfile +45 -0
- data/README.md +2 -2
- data/custom_example.rb +38 -9
- data/docker_cmd.sh +7 -0
- data/exe/run_tests.rb +23 -0
- data/img/screenshot1.png +0 -0
- data/img/screenshot2.png +0 -0
- data/lib/vimamsa/actions.rb +8 -0
- data/lib/vimamsa/buffer.rb +165 -53
- data/lib/vimamsa/buffer_changetext.rb +68 -14
- data/lib/vimamsa/buffer_cursor.rb +9 -3
- data/lib/vimamsa/buffer_list.rb +14 -28
- data/lib/vimamsa/buffer_manager.rb +1 -1
- data/lib/vimamsa/conf.rb +33 -1
- data/lib/vimamsa/diff_buffer.rb +185 -0
- data/lib/vimamsa/editor.rb +149 -80
- data/lib/vimamsa/file_finder.rb +6 -2
- data/lib/vimamsa/gui.rb +330 -135
- data/lib/vimamsa/gui_dialog.rb +2 -0
- data/lib/vimamsa/gui_file_panel.rb +94 -0
- data/lib/vimamsa/gui_form_generator.rb +4 -2
- data/lib/vimamsa/gui_func_panel.rb +127 -0
- data/lib/vimamsa/gui_image.rb +2 -4
- data/lib/vimamsa/gui_menu.rb +54 -1
- data/lib/vimamsa/gui_select_window.rb +18 -6
- data/lib/vimamsa/gui_settings.rb +486 -0
- data/lib/vimamsa/gui_sourceview.rb +196 -8
- data/lib/vimamsa/gui_text.rb +0 -22
- data/lib/vimamsa/hyper_plain_text.rb +1 -0
- data/lib/vimamsa/key_actions.rb +54 -31
- data/lib/vimamsa/key_binding_tree.rb +154 -8
- data/lib/vimamsa/key_bindings_vimlike.rb +48 -35
- data/lib/vimamsa/langservp.rb +161 -7
- data/lib/vimamsa/macro.rb +54 -7
- data/lib/vimamsa/main.rb +1 -0
- data/lib/vimamsa/rbvma.rb +5 -0
- data/lib/vimamsa/string_util.rb +56 -0
- data/lib/vimamsa/test_framework.rb +137 -0
- data/lib/vimamsa/util.rb +3 -36
- data/lib/vimamsa/version.rb +1 -1
- data/modules/calculator/calculator.rb +318 -0
- data/modules/calculator/calculator_info.rb +3 -0
- data/modules/terminal/terminal.rb +140 -0
- data/modules/terminal/terminal_info.rb +3 -0
- data/run_tests.rb +89 -0
- data/styles/dark.xml +1 -1
- data/styles/molokai_edit.xml +2 -2
- data/tests/key_bindings.rb +2 -0
- data/tests/test_basic_editing.rb +86 -0
- data/tests/test_copy_paste.rb +88 -0
- data/tests/test_key_bindings.rb +152 -0
- data/tests/test_module_interface.rb +98 -0
- data/tests/test_undo.rb +201 -0
- data/vimamsa.gemspec +6 -5
- metadata +52 -14
data/lib/vimamsa/macro.rb
CHANGED
|
@@ -26,22 +26,51 @@ class Macro
|
|
|
26
26
|
attr_reader :running_macro
|
|
27
27
|
attr_accessor :recorded_macros, :recording, :named_macros, :last_macro
|
|
28
28
|
|
|
29
|
+
NAMED_MACROS_FILE = "named_macros.json"
|
|
30
|
+
|
|
29
31
|
def initialize()
|
|
30
32
|
@recording = false
|
|
31
|
-
# @recorded_macros = {}
|
|
32
33
|
@current_recording = []
|
|
33
34
|
@current_name = nil
|
|
34
35
|
@last_macro = "a"
|
|
35
36
|
@running_macro = false
|
|
36
37
|
|
|
37
|
-
#TODO:
|
|
38
38
|
@recorded_macros = vma.marshal_load("macros", {})
|
|
39
|
-
@named_macros =
|
|
39
|
+
@named_macros = load_named_macros
|
|
40
40
|
vma.hook.register(:shutdown, self.method("save"))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def named_macros_path
|
|
44
|
+
get_dot_path(NAMED_MACROS_FILE)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Save named macros as JSON immediately — called automatically after name_macro.
|
|
48
|
+
def save_named_macros
|
|
49
|
+
require "json"
|
|
50
|
+
File.write(named_macros_path, JSON.pretty_generate(@named_macros))
|
|
51
|
+
rescue => e
|
|
52
|
+
error("Failed to save named macros: #{e}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Load named macros from JSON. Falls back to Marshal data from older versions.
|
|
56
|
+
def load_named_macros
|
|
57
|
+
require "json"
|
|
58
|
+
path = named_macros_path
|
|
59
|
+
if File.exist?(path)
|
|
60
|
+
data = JSON.parse(File.read(path))
|
|
61
|
+
# JSON keys are always strings; action lists are arrays of strings — correct types
|
|
62
|
+
return data
|
|
63
|
+
end
|
|
64
|
+
# Fallback: migrate from old Marshal-based storage
|
|
65
|
+
vma.marshal_load("named_macros", {})
|
|
66
|
+
rescue => e
|
|
67
|
+
error("Failed to load named macros: #{e}")
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
|
|
43
71
|
def save()
|
|
44
72
|
vma.marshal_save("macros", @recorded_macros)
|
|
73
|
+
# named_macros are kept current via save_named_macros; save a Marshal copy as backup
|
|
45
74
|
vma.marshal_save("named_macros", @named_macros)
|
|
46
75
|
end
|
|
47
76
|
|
|
@@ -56,17 +85,31 @@ class Macro
|
|
|
56
85
|
$macro_search_list = l
|
|
57
86
|
$select_keys = ["h", "l", "f", "d", "s", "a", "g", "z"]
|
|
58
87
|
|
|
88
|
+
delete_cb = proc { |name, refresh|
|
|
89
|
+
Gui.confirm("Delete macro '#{name}'?", proc {
|
|
90
|
+
vma.macro.delete_named_macro(name)
|
|
91
|
+
refresh.call
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
gui_select_update_window(l, $select_keys.collect { |x| x.upcase },
|
|
60
96
|
"gui_find_macro_select_callback",
|
|
61
|
-
"gui_find_macro_update_callback"
|
|
97
|
+
"gui_find_macro_update_callback",
|
|
98
|
+
{ delete_callback: delete_cb })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def delete_named_macro(name)
|
|
102
|
+
@named_macros.delete(name)
|
|
103
|
+
save_named_macros
|
|
104
|
+
message("Macro '#{name}' deleted")
|
|
62
105
|
end
|
|
63
106
|
|
|
64
107
|
def name_macro(name, id = nil)
|
|
65
108
|
debug "NAME MACRO #{name}"
|
|
66
|
-
if id.nil?
|
|
67
|
-
id = @last_macro
|
|
68
|
-
end
|
|
109
|
+
id = @last_macro if id.nil?
|
|
69
110
|
@named_macros[name] = @recorded_macros[id].clone
|
|
111
|
+
save_named_macros
|
|
112
|
+
message("Macro '#{name}' saved")
|
|
70
113
|
end
|
|
71
114
|
|
|
72
115
|
def start_recording(name)
|
|
@@ -122,6 +165,8 @@ class Macro
|
|
|
122
165
|
isok = true
|
|
123
166
|
# if acts.kind_of?(Array) and acts.any?
|
|
124
167
|
if acts.any?
|
|
168
|
+
vma.buf&.new_undo_group
|
|
169
|
+
vma.buf&.instance_variable_set(:@macro_group_active, true)
|
|
125
170
|
@running_macro = true
|
|
126
171
|
# TODO:needed?
|
|
127
172
|
# set_last_command({ method: vma.macro.method("run_macro"), params: [name] })
|
|
@@ -138,6 +183,8 @@ class Macro
|
|
|
138
183
|
end
|
|
139
184
|
end
|
|
140
185
|
@running_macro = false
|
|
186
|
+
vma.buf&.instance_variable_set(:@macro_group_active, false)
|
|
187
|
+
vma.buf&.new_undo_group
|
|
141
188
|
buf.set_pos(buf.pos)
|
|
142
189
|
# TODO: Should be a better way to trigger this. Sometimes need to wait for GTK to process things before updating the cursor.
|
|
143
190
|
run_as_idle proc { vma.buf.refresh_cursor; vma.buf.refresh_cursor }, delay: 0.15
|
data/lib/vimamsa/main.rb
CHANGED
data/lib/vimamsa/rbvma.rb
CHANGED
|
@@ -16,6 +16,7 @@ require "cgi"
|
|
|
16
16
|
require "uri"
|
|
17
17
|
require "vimamsa/conf"
|
|
18
18
|
require "vimamsa/util"
|
|
19
|
+
require "vimamsa/string_util"
|
|
19
20
|
# exit!
|
|
20
21
|
require "vimamsa/main"
|
|
21
22
|
require "vimamsa/terminal"
|
|
@@ -30,6 +31,9 @@ require "vimamsa/gui_form_generator"
|
|
|
30
31
|
require "vimamsa/gui_text"
|
|
31
32
|
require "vimamsa/gui_menu"
|
|
32
33
|
require "vimamsa/gui_dialog"
|
|
34
|
+
require "vimamsa/gui_settings"
|
|
35
|
+
require "vimamsa/gui_file_panel"
|
|
36
|
+
require "vimamsa/gui_func_panel"
|
|
33
37
|
require "vimamsa/gui_select_window"
|
|
34
38
|
require "vimamsa/gui_sourceview"
|
|
35
39
|
require "vimamsa/gui_sourceview_autocomplete"
|
|
@@ -46,6 +50,7 @@ require "vimamsa/buffer_manager"
|
|
|
46
50
|
require "vimamsa/constants"
|
|
47
51
|
require "vimamsa/debug"
|
|
48
52
|
require "vimamsa/tests"
|
|
53
|
+
require "vimamsa/test_framework"
|
|
49
54
|
require "vimamsa/easy_jump"
|
|
50
55
|
require "vimamsa/encrypt"
|
|
51
56
|
require "vimamsa/file_finder"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
def flip_true_false(str)
|
|
2
|
+
str.gsub(/\b(true|false)\b/i) do |match|
|
|
3
|
+
if match.match?(/\Atrue\z/i)
|
|
4
|
+
replacement = "false"
|
|
5
|
+
else
|
|
6
|
+
replacement = "true"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# preserve casing style
|
|
10
|
+
if match == match.upcase
|
|
11
|
+
replacement.upcase
|
|
12
|
+
elsif match[0] == match[0].upcase
|
|
13
|
+
replacement.capitalize
|
|
14
|
+
else
|
|
15
|
+
replacement.downcase
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_camel_case(str)
|
|
21
|
+
words = str.split(/\W+/) # Split the input string into words
|
|
22
|
+
camel_case_words = words.map.with_index do |word, index|
|
|
23
|
+
index == 0 ? word.downcase : word.capitalize
|
|
24
|
+
end
|
|
25
|
+
camel_case_words.join
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get all indexes for start of matching regexp
|
|
29
|
+
def scan_indexes(txt, regex)
|
|
30
|
+
# indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) + 1 }
|
|
31
|
+
indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) }
|
|
32
|
+
return indexes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def is_path(s)
|
|
36
|
+
m = s.match(/(~[a-z]*)?\/.*\//)
|
|
37
|
+
if m != nil
|
|
38
|
+
return true
|
|
39
|
+
end
|
|
40
|
+
return false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sanitize_input(str)
|
|
45
|
+
if str.encoding != Encoding::UTF_8
|
|
46
|
+
str = text.encode(Encoding::UTF_8)
|
|
47
|
+
end
|
|
48
|
+
str.gsub!(/\r\n/, "\n")
|
|
49
|
+
return str
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def is_url(s)
|
|
53
|
+
return s.match(/(https?|file):\/\/.*/) != nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# VmaTest — integration test base class
|
|
2
|
+
#
|
|
3
|
+
# Tests run inside the live GTK app (headless or normal) after vma.start.
|
|
4
|
+
# Each test method gets a fresh empty buffer.
|
|
5
|
+
#
|
|
6
|
+
# Two ways to drive the editor:
|
|
7
|
+
# act(:action_name) — execute a registered action directly
|
|
8
|
+
# act("buf.insert_txt(…)") — eval a string action
|
|
9
|
+
# keys("i h e l l o esc") — simulate a space-separated key sequence
|
|
10
|
+
#
|
|
11
|
+
# Assertions raise VmaTestFailure on mismatch (caught by the runner).
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# Run all tests: run_vma_tests
|
|
15
|
+
# Run one class: run_vma_tests(MyTests)
|
|
16
|
+
|
|
17
|
+
class VmaTestFailure < StandardError; end
|
|
18
|
+
|
|
19
|
+
class VmaTest
|
|
20
|
+
attr_reader :failures, :passes
|
|
21
|
+
|
|
22
|
+
# Subclasses define test_* methods.
|
|
23
|
+
# Each receives a fresh buffer and the kbd in :command mode.
|
|
24
|
+
def run_all
|
|
25
|
+
@passes = 0
|
|
26
|
+
@failures = []
|
|
27
|
+
test_methods = self.class.instance_methods(false)
|
|
28
|
+
.select { |m| m.to_s.start_with?("test_") }
|
|
29
|
+
.sort
|
|
30
|
+
|
|
31
|
+
test_methods.each do |m|
|
|
32
|
+
_setup_test
|
|
33
|
+
begin
|
|
34
|
+
send(m)
|
|
35
|
+
@passes += 1
|
|
36
|
+
puts " PASS #{self.class}##{m}"
|
|
37
|
+
rescue VmaTestFailure => e
|
|
38
|
+
@failures << "#{self.class}##{m}: #{e.message}"
|
|
39
|
+
puts " FAIL #{self.class}##{m}: #{e.message}"
|
|
40
|
+
rescue => e
|
|
41
|
+
@failures << "#{self.class}##{m}: #{e.class}: #{e.message}"
|
|
42
|
+
puts " ERROR #{self.class}##{m}: #{e.class}: #{e.message}"
|
|
43
|
+
puts e.backtrace.first(5).join("\n")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ── Actions ──────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
# Execute one action (symbol, string, or proc)
|
|
51
|
+
def act(action)
|
|
52
|
+
exec_action(action)
|
|
53
|
+
drain_idle
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Simulate a space-separated key sequence in the current mode.
|
|
57
|
+
# E.g.: keys("i h e l l o esc")
|
|
58
|
+
# Special tokens: ctrl-x, alt-x, shift-X, esc, enter, backspace, tab, space
|
|
59
|
+
def keys(seq)
|
|
60
|
+
seq.split.each do |k|
|
|
61
|
+
vma.kbd.match_key_conf(k, nil, :key_press)
|
|
62
|
+
# Emit a key_release for modifier-only keys so state resets correctly
|
|
63
|
+
vma.kbd.match_key_conf(k + "!", nil, :key_release) if %w[ctrl alt shift].include?(k)
|
|
64
|
+
end
|
|
65
|
+
drain_idle
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ── Assertions ───────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
def assert_buf(expected, msg = nil)
|
|
71
|
+
actual = vma.buf.to_s
|
|
72
|
+
return if actual == expected
|
|
73
|
+
raise VmaTestFailure, (msg || "buffer mismatch\n expected: #{expected.inspect}\n actual: #{actual.inspect}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assert_pos(lpos, cpos, msg = nil)
|
|
77
|
+
al = vma.buf.lpos
|
|
78
|
+
ac = vma.buf.cpos
|
|
79
|
+
return if al == lpos && ac == cpos
|
|
80
|
+
raise VmaTestFailure, (msg || "position mismatch: expected line #{lpos} col #{cpos}, got line #{al} col #{ac}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def assert_mode(expected_mode, msg = nil)
|
|
84
|
+
actual = vma.kbd.get_mode
|
|
85
|
+
return if actual == expected_mode
|
|
86
|
+
raise VmaTestFailure, (msg || "mode mismatch: expected #{expected_mode.inspect}, got #{actual.inspect}")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def assert_eq(expected, actual, msg = nil)
|
|
90
|
+
return if expected == actual
|
|
91
|
+
raise VmaTestFailure, (msg || "expected #{expected.inspect}, got #{actual.inspect}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assert(cond, msg = "assertion failed")
|
|
95
|
+
raise VmaTestFailure, msg unless cond
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def _setup_test
|
|
101
|
+
# Fresh buffer, command mode
|
|
102
|
+
b = create_new_buffer("\n", "test", true)
|
|
103
|
+
vma.kbd.set_mode(:command) rescue nil
|
|
104
|
+
drain_idle
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Let any GLib::Idle callbacks run before we check state
|
|
108
|
+
def drain_idle
|
|
109
|
+
20.times { GLib::MainContext.default.iteration(false) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ── Runner ───────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def run_vma_tests(*classes)
|
|
116
|
+
classes = ObjectSpace.each_object(Class)
|
|
117
|
+
.select { |c| c < VmaTest }
|
|
118
|
+
.sort_by(&:name) if classes.empty?
|
|
119
|
+
|
|
120
|
+
total_pass = 0
|
|
121
|
+
total_fail = []
|
|
122
|
+
|
|
123
|
+
classes.each do |klass|
|
|
124
|
+
puts "\n#{klass}"
|
|
125
|
+
t = klass.new
|
|
126
|
+
t.run_all
|
|
127
|
+
total_pass += t.passes
|
|
128
|
+
total_fail.concat(t.failures)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
puts "\n#{"=" * 50}"
|
|
132
|
+
puts "Results: #{total_pass} passed, #{total_fail.size} failed"
|
|
133
|
+
total_fail.each { |f| puts " FAIL: #{f}" }
|
|
134
|
+
puts "=" * 50
|
|
135
|
+
|
|
136
|
+
total_fail.empty?
|
|
137
|
+
end
|
data/lib/vimamsa/util.rb
CHANGED
|
@@ -4,7 +4,9 @@ VOWELS = %w(a e i o u)
|
|
|
4
4
|
CONSONANTS = %w(b c d f g h j k l m n p q r s t v w x y z)
|
|
5
5
|
|
|
6
6
|
def draw_cursor_bug_workaround()
|
|
7
|
-
|
|
7
|
+
if !vma.gui.nil?
|
|
8
|
+
DelayExecutioner.exec(id: :bug_workaround_draw_cursor, wait: 1.0, callable: proc { vma.gui.view.draw_cursor(); debug ":bug_workaround_draw_cursor"; false })
|
|
9
|
+
end
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def running_wayland?
|
|
@@ -24,14 +26,6 @@ def tilde_path(abspath)
|
|
|
24
26
|
abspath.sub(/^#{Regexp.escape(userhome)}\//, "~/")
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def to_camel_case(str)
|
|
28
|
-
words = str.split(/\W+/) # Split the input string into words
|
|
29
|
-
camel_case_words = words.map.with_index do |word, index|
|
|
30
|
-
index == 0 ? word.downcase : word.capitalize
|
|
31
|
-
end
|
|
32
|
-
camel_case_words.join
|
|
33
|
-
end
|
|
34
|
-
|
|
35
29
|
def generate_password(length)
|
|
36
30
|
password = ""
|
|
37
31
|
while password.size < length
|
|
@@ -53,13 +47,6 @@ def generate_password_to_buf(length)
|
|
|
53
47
|
vma.buf.insert_txt(passw)
|
|
54
48
|
end
|
|
55
49
|
|
|
56
|
-
# Get all indexes for start of matching regexp
|
|
57
|
-
def scan_indexes(txt, regex)
|
|
58
|
-
# indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) + 1 }
|
|
59
|
-
indexes = txt.enum_for(:scan, regex).map { Regexp.last_match.begin(0) }
|
|
60
|
-
return indexes
|
|
61
|
-
end
|
|
62
|
-
|
|
63
50
|
def file_mime_type(fpath)
|
|
64
51
|
fpath = File.expand_path(fpath)
|
|
65
52
|
return nil if !File.readable?(fpath)
|
|
@@ -322,18 +309,6 @@ def read_file(text, path)
|
|
|
322
309
|
return content
|
|
323
310
|
end
|
|
324
311
|
|
|
325
|
-
def sanitize_input(str)
|
|
326
|
-
if str.encoding != Encoding::UTF_8
|
|
327
|
-
str = text.encode(Encoding::UTF_8)
|
|
328
|
-
end
|
|
329
|
-
str.gsub!(/\r\n/, "\n")
|
|
330
|
-
return str
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def is_url(s)
|
|
334
|
-
return s.match(/(https?|file):\/\/.*/) != nil
|
|
335
|
-
end
|
|
336
|
-
|
|
337
312
|
def expand_if_existing(fpath)
|
|
338
313
|
return nil if fpath.class != String
|
|
339
314
|
fpath = File.expand_path(fpath)
|
|
@@ -386,11 +361,3 @@ end
|
|
|
386
361
|
# #TODO: check contents of file
|
|
387
362
|
# return true
|
|
388
363
|
# end
|
|
389
|
-
|
|
390
|
-
def is_path(s)
|
|
391
|
-
m = s.match(/(~[a-z]*)?\/.*\//)
|
|
392
|
-
if m != nil
|
|
393
|
-
return true
|
|
394
|
-
end
|
|
395
|
-
return false
|
|
396
|
-
end
|
data/lib/vimamsa/version.rb
CHANGED