vimamsa 0.1.23 → 0.1.25
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/custom_example.rb +37 -11
- data/docker_cmd.sh +7 -0
- data/exe/run_tests.rb +23 -0
- data/lib/vimamsa/actions.rb +8 -0
- data/lib/vimamsa/buffer.rb +38 -47
- data/lib/vimamsa/buffer_changetext.rb +49 -12
- data/lib/vimamsa/buffer_list.rb +2 -28
- data/lib/vimamsa/conf.rb +30 -0
- data/lib/vimamsa/diff_buffer.rb +80 -32
- data/lib/vimamsa/editor.rb +54 -67
- data/lib/vimamsa/file_finder.rb +6 -2
- data/lib/vimamsa/gui.rb +247 -63
- data/lib/vimamsa/gui_file_panel.rb +1 -0
- data/lib/vimamsa/gui_func_panel.rb +127 -0
- data/lib/vimamsa/gui_menu.rb +42 -0
- data/lib/vimamsa/gui_select_window.rb +17 -6
- data/lib/vimamsa/gui_settings.rb +347 -13
- data/lib/vimamsa/gui_sourceview.rb +116 -2
- data/lib/vimamsa/gui_text.rb +0 -22
- data/lib/vimamsa/hyper_plain_text.rb +1 -0
- data/lib/vimamsa/key_actions.rb +30 -29
- data/lib/vimamsa/key_binding_tree.rb +85 -3
- data/lib/vimamsa/key_bindings_vimlike.rb +4 -0
- data/lib/vimamsa/langservp.rb +161 -7
- data/lib/vimamsa/macro.rb +54 -7
- data/lib/vimamsa/rbvma.rb +2 -0
- data/lib/vimamsa/test_framework.rb +137 -0
- 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 +46 -14
|
@@ -30,14 +30,14 @@ class SelectUpdateWindow
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def set_selected_row(rownum)
|
|
33
|
+
count = @model.count
|
|
34
|
+
return if count == 0
|
|
33
35
|
rownum = 0 if rownum < 0
|
|
36
|
+
rownum = count - 1 if rownum >= count
|
|
34
37
|
@selected_row = rownum
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
iter = @model.get_iter(path)
|
|
39
|
-
@tv.selection.select_iter(iter)
|
|
40
|
-
end
|
|
38
|
+
path = Gtk::TreePath.new(@selected_row.to_s)
|
|
39
|
+
iter = @model.get_iter(path)
|
|
40
|
+
@tv.selection.select_iter(iter)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def initialize(main_window, item_list, jump_keys, select_callback, update_callback, opt = {})
|
|
@@ -126,6 +126,17 @@ class SelectUpdateWindow
|
|
|
126
126
|
@window.destroy
|
|
127
127
|
# debug iter[1].inspect
|
|
128
128
|
true
|
|
129
|
+
elsif keyval == Gdk::Keyval::KEY_Delete && @opt[:delete_callback]
|
|
130
|
+
path = Gtk::TreePath.new(@selected_row.to_s)
|
|
131
|
+
iter = @model.get_iter(path)
|
|
132
|
+
next false unless iter
|
|
133
|
+
name = iter[COLUMN_DESCRIPTION]
|
|
134
|
+
refresh = proc {
|
|
135
|
+
update_item_list(@update_callback.call(@entry.text))
|
|
136
|
+
@window.present
|
|
137
|
+
}
|
|
138
|
+
@opt[:delete_callback].call(name, refresh)
|
|
139
|
+
true
|
|
129
140
|
elsif keyval == Gdk::Keyval::KEY_Escape
|
|
130
141
|
@window.destroy
|
|
131
142
|
true
|
data/lib/vimamsa/gui_settings.rb
CHANGED
|
@@ -19,6 +19,14 @@ SETTINGS_DEFS = [
|
|
|
19
19
|
:settings => [
|
|
20
20
|
{ :key => [:match, :highlight, :color], :label => "Search highlight color", :type => :string },
|
|
21
21
|
{ :key => [:kbd, :show_prev_action], :label => "Show previous action in toolbar", :type => :bool },
|
|
22
|
+
{ :key => [:style_scheme], :label => "Color scheme", :type => :select,
|
|
23
|
+
:options => proc {
|
|
24
|
+
ssm = GtkSource::StyleSchemeManager.new
|
|
25
|
+
ssm.set_search_path(ssm.search_path << ppath("styles/"))
|
|
26
|
+
ssm.scheme_ids.reject { |id| [VIMAMSA_OVERLAY_SCHEME_ID, VIMAMSA_CONTRAST_SCHEME_ID].include?(id) }.sort
|
|
27
|
+
} },
|
|
28
|
+
{ :key => [:color_contrast], :label => "Color scheme contrast (1.0 = original)", :type => :float, :min => 0.5, :max => 2.0, :step => 0.05 },
|
|
29
|
+
{ :key => [:color_brightness], :label => "Color scheme brightness (0.0 = original)", :type => :float, :min => -0.5, :max => 0.5, :step => 0.01 },
|
|
22
30
|
],
|
|
23
31
|
},
|
|
24
32
|
{
|
|
@@ -27,10 +35,40 @@ SETTINGS_DEFS = [
|
|
|
27
35
|
{ :key => [:lsp, :enabled], :label => "Enable LSP (Language Server)", :type => :bool },
|
|
28
36
|
{ :key => [:experimental], :label => "Enable experimental features", :type => :bool },
|
|
29
37
|
{ :key => [:macro, :animation_delay], :label => "Macro animation delay (sec)", :type => :float, :min => 0.0, :max => 2.0, :step => 0.0001 },
|
|
38
|
+
{ :key => [:paste, :cursor_at_start], :label => "Leave cursor at start of pasted text", :type => :bool },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
:label => "Files",
|
|
43
|
+
:settings => [
|
|
44
|
+
{ :key => ["search_dirs"], :label => "Search directories (one per line)", :type => :string_list },
|
|
30
45
|
],
|
|
31
46
|
},
|
|
32
47
|
]
|
|
33
48
|
|
|
49
|
+
# Build the full settings definition list, appending a Modules section
|
|
50
|
+
# dynamically from whatever subdirectories exist in modules/.
|
|
51
|
+
def all_settings_defs
|
|
52
|
+
# Build one settings entry per module found in modules/*/.
|
|
53
|
+
# Each module may provide a <name>_info.rb file that defines <name>_info,
|
|
54
|
+
# returning a hash with :name (display label) and :no_restart (bool).
|
|
55
|
+
# Modules without an info file fall back to a capitalized directory name
|
|
56
|
+
# and require a restart to activate.
|
|
57
|
+
module_settings = Dir.glob(ppath("modules/*/")).sort.map do |mod_dir|
|
|
58
|
+
mod_name = File.basename(mod_dir)
|
|
59
|
+
info_file = File.join(mod_dir, "#{mod_name}_info.rb")
|
|
60
|
+
load info_file if File.exist?(info_file)
|
|
61
|
+
info_fn = "#{mod_name}_info"
|
|
62
|
+
info = respond_to?(info_fn, true) ? send(info_fn) : {}
|
|
63
|
+
label = info[:name] || mod_name.split(/[_\-]/).map(&:capitalize).join(" ")
|
|
64
|
+
no_restart = info[:no_restart] || false
|
|
65
|
+
restart_note = no_restart ? "" : " (requires restart)"
|
|
66
|
+
{ :key => [:modules, mod_name.to_sym, :enabled], :label => "#{label}#{restart_note}", :type => :bool, :no_restart => no_restart }
|
|
67
|
+
end
|
|
68
|
+
return SETTINGS_DEFS unless module_settings.any?
|
|
69
|
+
SETTINGS_DEFS + [{ :label => "Modules", :settings => module_settings }]
|
|
70
|
+
end
|
|
71
|
+
|
|
34
72
|
class SettingsDialog
|
|
35
73
|
def initialize
|
|
36
74
|
@widgets = {}
|
|
@@ -47,7 +85,7 @@ class SettingsDialog
|
|
|
47
85
|
notebook = Gtk::Notebook.new
|
|
48
86
|
outer.append(notebook)
|
|
49
87
|
|
|
50
|
-
|
|
88
|
+
all_settings_defs.each do |section|
|
|
51
89
|
grid = Gtk::Grid.new
|
|
52
90
|
grid.row_spacing = 10
|
|
53
91
|
grid.column_spacing = 16
|
|
@@ -56,16 +94,28 @@ class SettingsDialog
|
|
|
56
94
|
grid.margin_start = 12
|
|
57
95
|
grid.margin_end = 12
|
|
58
96
|
|
|
59
|
-
|
|
97
|
+
grid_row = 0
|
|
98
|
+
section[:settings].each do |s|
|
|
60
99
|
label = Gtk::Label.new(s[:label])
|
|
61
100
|
label.halign = :start
|
|
62
101
|
label.hexpand = true
|
|
63
102
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
103
|
+
if s[:type] == :string_list
|
|
104
|
+
# Label spans both columns; editor on the next row spanning both columns
|
|
105
|
+
grid.attach(label, 0, grid_row, 2, 1)
|
|
106
|
+
grid_row += 1
|
|
107
|
+
cur = get(s[:key])
|
|
108
|
+
container, get_value = build_string_list_editor(cur.is_a?(Array) ? cur : [])
|
|
109
|
+
@widgets[s[:key]] = { :widget => container, :type => s[:type], :get_value => get_value }
|
|
110
|
+
grid.attach(container, 0, grid_row, 2, 1)
|
|
111
|
+
else
|
|
112
|
+
widget = make_widget(s)
|
|
113
|
+
# Store prev_value so save_and_close can detect newly-enabled modules.
|
|
114
|
+
@widgets[s[:key]] = { :widget => widget, :type => s[:type], :no_restart => s[:no_restart], :prev_value => cnf_get(s[:key]) }
|
|
115
|
+
grid.attach(label, 0, grid_row, 1, 1)
|
|
116
|
+
grid.attach(widget, 1, grid_row, 1, 1)
|
|
117
|
+
end
|
|
118
|
+
grid_row += 1
|
|
69
119
|
end
|
|
70
120
|
|
|
71
121
|
notebook.append_page(grid, Gtk::Label.new(section[:label]))
|
|
@@ -97,7 +147,7 @@ class SettingsDialog
|
|
|
97
147
|
end
|
|
98
148
|
|
|
99
149
|
def make_widget(s)
|
|
100
|
-
cur =
|
|
150
|
+
cur = cnf_get(s[:key])
|
|
101
151
|
case s[:type]
|
|
102
152
|
when :bool
|
|
103
153
|
w = Gtk::Switch.new
|
|
@@ -116,24 +166,122 @@ class SettingsDialog
|
|
|
116
166
|
w = Gtk::Entry.new
|
|
117
167
|
w.text = cur.to_s
|
|
118
168
|
w
|
|
169
|
+
when :select
|
|
170
|
+
options = s[:options].is_a?(Proc) ? s[:options].call : s[:options]
|
|
171
|
+
cur = cnf_get(s[:key]).to_s
|
|
172
|
+
string_list = Gtk::StringList.new(options)
|
|
173
|
+
w = Gtk::DropDown.new(string_list, nil)
|
|
174
|
+
w.selected = [options.index(cur) || 0, 0].max
|
|
175
|
+
w
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_string_list_editor(paths)
|
|
180
|
+
store = Gtk::ListStore.new(String)
|
|
181
|
+
paths.each { |p| store.append[0] = p }
|
|
182
|
+
|
|
183
|
+
tv = Gtk::TreeView.new(store)
|
|
184
|
+
tv.headers_visible = false
|
|
185
|
+
renderer = Gtk::CellRendererText.new
|
|
186
|
+
renderer.ellipsize = Pango::EllipsizeMode::START
|
|
187
|
+
col = Gtk::TreeViewColumn.new("", renderer, text: 0)
|
|
188
|
+
col.expand = true
|
|
189
|
+
tv.append_column(col)
|
|
190
|
+
|
|
191
|
+
sw = Gtk::ScrolledWindow.new
|
|
192
|
+
sw.set_policy(:automatic, :automatic)
|
|
193
|
+
sw.set_child(tv)
|
|
194
|
+
sw.set_size_request(-1, 120)
|
|
195
|
+
sw.vexpand = true
|
|
196
|
+
|
|
197
|
+
add_btn = Gtk::Button.new(label: "Add…")
|
|
198
|
+
add_btn.signal_connect("clicked") do
|
|
199
|
+
chooser = Gtk::FileChooserDialog.new(
|
|
200
|
+
:title => "Select directory",
|
|
201
|
+
:action => :select_folder,
|
|
202
|
+
:buttons => [["Select", :accept], ["Cancel", :cancel]],
|
|
203
|
+
)
|
|
204
|
+
chooser.set_transient_for(@window)
|
|
205
|
+
chooser.modal = true
|
|
206
|
+
chooser.signal_connect("response") do |dlg, resp|
|
|
207
|
+
if resp == Gtk::ResponseType::ACCEPT
|
|
208
|
+
iter = store.append
|
|
209
|
+
iter[0] = dlg.file.path
|
|
210
|
+
end
|
|
211
|
+
dlg.destroy
|
|
212
|
+
end
|
|
213
|
+
chooser.show
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
remove_btn = Gtk::Button.new(label: "Remove")
|
|
217
|
+
remove_btn.signal_connect("clicked") do
|
|
218
|
+
sel = tv.selection.selected
|
|
219
|
+
store.remove(sel) if sel
|
|
119
220
|
end
|
|
221
|
+
|
|
222
|
+
btn_box = Gtk::Box.new(:horizontal, 6)
|
|
223
|
+
btn_box.append(add_btn)
|
|
224
|
+
btn_box.append(remove_btn)
|
|
225
|
+
|
|
226
|
+
container = Gtk::Box.new(:vertical, 4)
|
|
227
|
+
container.vexpand = true
|
|
228
|
+
container.append(sw)
|
|
229
|
+
container.append(btn_box)
|
|
230
|
+
|
|
231
|
+
get_value = proc {
|
|
232
|
+
result = []
|
|
233
|
+
store.each { |_m, _path, iter| result << iter[0] }
|
|
234
|
+
result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
[container, get_value]
|
|
120
238
|
end
|
|
121
239
|
|
|
122
240
|
def save_and_close
|
|
123
241
|
@widgets.each do |key, info|
|
|
124
242
|
val = case info[:type]
|
|
125
|
-
when :bool
|
|
126
|
-
when :int
|
|
127
|
-
when :float
|
|
128
|
-
when :string
|
|
243
|
+
when :bool then info[:widget].active?
|
|
244
|
+
when :int then info[:widget].value.to_i
|
|
245
|
+
when :float then info[:widget].value.to_f
|
|
246
|
+
when :string then info[:widget].text
|
|
247
|
+
when :string_list then info[:get_value].call
|
|
248
|
+
when :select then info[:widget].selected_item&.string
|
|
129
249
|
end
|
|
130
|
-
|
|
250
|
+
cnf_set(key, val) unless val.nil?
|
|
131
251
|
end
|
|
132
252
|
save_settings_to_file
|
|
133
253
|
gui_refresh_font
|
|
254
|
+
gui_refresh_style_scheme
|
|
255
|
+
activate_no_restart_modules
|
|
134
256
|
@window.destroy
|
|
135
257
|
end
|
|
136
258
|
|
|
259
|
+
# For modules that support live activation (no_restart: true), apply enable
|
|
260
|
+
# and disable transitions immediately without requiring a restart.
|
|
261
|
+
# Modules that were already in the target state when the dialog opened are
|
|
262
|
+
# skipped (prev_value matches new value), so we never double-init or double-disable.
|
|
263
|
+
def activate_no_restart_modules
|
|
264
|
+
@widgets.each do |key, info|
|
|
265
|
+
next unless key.is_a?(Array) && key[0] == :modules && key[2] == :enabled
|
|
266
|
+
next unless info[:no_restart] # module requires restart — skip
|
|
267
|
+
mod_name = key[1].to_s
|
|
268
|
+
was_enabled = info[:prev_value]
|
|
269
|
+
now_enabled = cnf_get(key)
|
|
270
|
+
if !was_enabled && now_enabled
|
|
271
|
+
# Toggled on: load the module file and call <name>_init.
|
|
272
|
+
main_file = ppath("modules/#{mod_name}/#{mod_name}.rb")
|
|
273
|
+
next unless File.exist?(main_file)
|
|
274
|
+
load main_file
|
|
275
|
+
init_fn = "#{mod_name}_init"
|
|
276
|
+
send(init_fn) if respond_to?(init_fn, true)
|
|
277
|
+
elsif was_enabled && !now_enabled
|
|
278
|
+
# Toggled off: call <name>_disable to unregister actions and bindings.
|
|
279
|
+
disable_fn = "#{mod_name}_disable"
|
|
280
|
+
send(disable_fn) if respond_to?(disable_fn, true)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
137
285
|
def run
|
|
138
286
|
@window.show
|
|
139
287
|
end
|
|
@@ -143,6 +291,192 @@ def show_settings_dialog
|
|
|
143
291
|
SettingsDialog.new.run
|
|
144
292
|
end
|
|
145
293
|
|
|
294
|
+
VIMAMSA_OVERLAY_SCHEME_ID = "vimamsa_overlay"
|
|
295
|
+
VIMAMSA_CONTRAST_SCHEME_ID = "vimamsa_contrast"
|
|
296
|
+
|
|
297
|
+
# ── Color utilities ────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
def hex_to_rgb(hex)
|
|
300
|
+
hex = hex.delete("#")
|
|
301
|
+
hex = hex.chars.map { |c| c * 2 }.join if hex.length == 3
|
|
302
|
+
[hex[0, 2].to_i(16), hex[2, 2].to_i(16), hex[4, 2].to_i(16)]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def rgb_to_hsl(r, g, b)
|
|
306
|
+
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
|
307
|
+
max, min = [r, g, b].max, [r, g, b].min
|
|
308
|
+
l = (max + min) / 2.0
|
|
309
|
+
if max == min
|
|
310
|
+
[0.0, 0.0, l]
|
|
311
|
+
else
|
|
312
|
+
d = max - min
|
|
313
|
+
s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min)
|
|
314
|
+
h = case max
|
|
315
|
+
when r then ((g - b) / d + (g < b ? 6 : 0)) / 6.0
|
|
316
|
+
when g then ((b - r) / d + 2) / 6.0
|
|
317
|
+
else ((r - g) / d + 4) / 6.0
|
|
318
|
+
end
|
|
319
|
+
[h, s, l]
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def hsl_to_rgb(h, s, l)
|
|
324
|
+
return Array.new(3, (l * 255).round) if s == 0
|
|
325
|
+
q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
326
|
+
p = 2 * l - q
|
|
327
|
+
[h + 1 / 3.0, h, h - 1 / 3.0].map do |t|
|
|
328
|
+
t += 1 if t < 0; t -= 1 if t > 1
|
|
329
|
+
v = if t < 1 / 6.0 then p + (q - p) * 6 * t
|
|
330
|
+
elsif t < 1 / 2.0 then q
|
|
331
|
+
elsif t < 2 / 3.0 then p + (q - p) * (2 / 3.0 - t) * 6
|
|
332
|
+
else p
|
|
333
|
+
end
|
|
334
|
+
(v * 255).round.clamp(0, 255)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Adjust contrast and brightness of a hex color.
|
|
339
|
+
# contrast: scales lightness around 0.5 (>1 increases separation, <1 reduces it).
|
|
340
|
+
# brightness: fixed lightness offset applied after contrast (-1.0..1.0, 0 = no change).
|
|
341
|
+
# Chroma is preserved by rescaling saturation to compensate for gamut narrowing at
|
|
342
|
+
# extreme lightness values, preventing warm colors from washing out.
|
|
343
|
+
def adjust_hex_color(hex, contrast, brightness)
|
|
344
|
+
r, g, b = hex_to_rgb(hex)
|
|
345
|
+
h, s, l = rgb_to_hsl(r, g, b)
|
|
346
|
+
new_l = (0.5 + (l - 0.5) * contrast + brightness).clamp(0.0, 1.0)
|
|
347
|
+
old_chroma = s * [l, 1.0 - l].min * 2.0
|
|
348
|
+
max_new_chroma = [new_l, 1.0 - new_l].min * 2.0
|
|
349
|
+
new_s = max_new_chroma > 0 ? [old_chroma / max_new_chroma, 1.0].min : 0.0
|
|
350
|
+
nr, ng, nb = hsl_to_rgb(h, new_s, new_l)
|
|
351
|
+
"#%02X%02X%02X" % [nr, ng, nb]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# ── Scheme XML processing ──────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
# Find the XML file on disk for a given scheme id.
|
|
357
|
+
def find_scheme_xml_path(scheme_id)
|
|
358
|
+
ssm = GtkSource::StyleSchemeManager.new
|
|
359
|
+
ssm.set_search_path(ssm.search_path << ppath("styles/"))
|
|
360
|
+
ssm.search_path.each do |dir|
|
|
361
|
+
next unless Dir.exist?(dir)
|
|
362
|
+
Dir[File.join(dir, "*.xml")].each do |f|
|
|
363
|
+
return f if File.read(f).include?("id=\"#{scheme_id}\"")
|
|
364
|
+
rescue StandardError
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
nil
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Apply contrast and brightness to every hex color value in an XML string.
|
|
371
|
+
def apply_contrast_to_xml(xml, contrast, brightness)
|
|
372
|
+
xml.gsub(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}\b/) do |hex|
|
|
373
|
+
adjust_hex_color(hex, contrast, brightness)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# If contrast != 1.0, generate a contrast-adjusted copy of base_scheme_id and
|
|
378
|
+
# return its scheme id. Otherwise return base_scheme_id unchanged.
|
|
379
|
+
def apply_contrast_transformation(base_scheme_id, contrast, brightness)
|
|
380
|
+
return base_scheme_id if contrast == 1.0 && brightness == 0.0
|
|
381
|
+
xml_path = find_scheme_xml_path(base_scheme_id)
|
|
382
|
+
return base_scheme_id if xml_path.nil?
|
|
383
|
+
|
|
384
|
+
adjusted = apply_contrast_to_xml(IO.read(xml_path), contrast, brightness)
|
|
385
|
+
adjusted = adjusted.sub(/(<style-scheme\b[^>]*)id="[^"]*"/, "\\1id=\"#{VIMAMSA_CONTRAST_SCHEME_ID}\"")
|
|
386
|
+
adjusted = adjusted.sub(/(<style-scheme\b[^>]*)name="[^"]*"/, "\\1name=\"#{VIMAMSA_CONTRAST_SCHEME_ID}\"")
|
|
387
|
+
adjusted = adjusted.sub(/\s*parent-scheme="[^"]*"/, "") # avoid inheritance chains
|
|
388
|
+
IO.write(ppath("styles/_vimamsa_contrast.xml"), adjusted)
|
|
389
|
+
VIMAMSA_CONTRAST_SCHEME_ID
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ── Overlay scheme ─────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
# Return foreground attribute XML fragment for a style name from a scheme object.
|
|
395
|
+
# Falls back to default_fg if the scheme doesn't define it.
|
|
396
|
+
def style_fg_attr(sty, style_name, default_fg = nil)
|
|
397
|
+
fg = sty&.get_style(style_name)&.foreground
|
|
398
|
+
fg = default_fg if fg.nil? || fg.empty?
|
|
399
|
+
fg ? " foreground=\"#{fg}\"" : ""
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Generate a GtkSourceView style scheme that inherits from base_scheme_id
|
|
403
|
+
# (optionally contrast-adjusted) and overlays Vimamsa-specific heading/
|
|
404
|
+
# hyperlink styles on top. Written to styles/_vimamsa_overlay.xml.
|
|
405
|
+
# Foreground colors are read from the parent scheme so they are preserved.
|
|
406
|
+
def generate_vimamsa_overlay(base_scheme_id)
|
|
407
|
+
contrast = (cnf_get([:color_contrast]) || 1.0).to_f
|
|
408
|
+
brightness = (cnf_get([:color_brightness]) || 0.0).to_f
|
|
409
|
+
parent_id = apply_contrast_transformation(base_scheme_id, contrast, brightness)
|
|
410
|
+
|
|
411
|
+
# Load the parent scheme to read existing foreground colors for headings/links
|
|
412
|
+
ssm = GtkSource::StyleSchemeManager.new
|
|
413
|
+
ssm.set_search_path(ssm.search_path << ppath("styles/"))
|
|
414
|
+
parent_sty = ssm.get_scheme(parent_id)
|
|
415
|
+
|
|
416
|
+
xml = <<~XML
|
|
417
|
+
<?xml version="1.0"?>
|
|
418
|
+
<style-scheme id="#{VIMAMSA_OVERLAY_SCHEME_ID}" name="#{VIMAMSA_OVERLAY_SCHEME_ID}" version="1.0" parent-scheme="#{parent_id}">
|
|
419
|
+
<style name="def:title" scale="2.0" bold="true"#{style_fg_attr(parent_sty, "def:title")}/>
|
|
420
|
+
<style name="def:hyperlink" bold="true"#{style_fg_attr(parent_sty, "def:hyperlink", "#4FC3F7")}/>
|
|
421
|
+
<style name="def:heading0" scale="2.0" bold="true"#{style_fg_attr(parent_sty, "def:heading0")}/>
|
|
422
|
+
<style name="def:heading1" scale="1.75" bold="true"#{style_fg_attr(parent_sty, "def:heading1")}/>
|
|
423
|
+
<style name="def:heading2" scale="1.5" bold="true"#{style_fg_attr(parent_sty, "def:heading2")}/>
|
|
424
|
+
<style name="def:heading3" scale="1.25" bold="true"#{style_fg_attr(parent_sty, "def:heading3")}/>
|
|
425
|
+
<style name="def:heading4" scale="1.175" bold="true"#{style_fg_attr(parent_sty, "def:heading4")}/>
|
|
426
|
+
<style name="def:heading5" scale="1.1" bold="true"#{style_fg_attr(parent_sty, "def:heading5")}/>
|
|
427
|
+
<style name="def:heading6" scale="1.0" bold="true"#{style_fg_attr(parent_sty, "def:heading6")}/>
|
|
428
|
+
<style name="def:bold" bold="true"/>
|
|
429
|
+
</style-scheme>
|
|
430
|
+
XML
|
|
431
|
+
user_styles_dir = File.expand_path("~/.config/vimamsa/styles")
|
|
432
|
+
FileUtils.mkdir_p(user_styles_dir)
|
|
433
|
+
IO.write(File.join(user_styles_dir, "_vimamsa_overlay.xml"), xml)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Build a StyleSchemeManager with the project styles dir appended,
|
|
437
|
+
# apply the overlay on top of the user's chosen base scheme,
|
|
438
|
+
# and return the overlay scheme object.
|
|
439
|
+
def load_vimamsa_scheme
|
|
440
|
+
base_id = cnf.style_scheme! || "molokai_edit"
|
|
441
|
+
generate_vimamsa_overlay(base_id)
|
|
442
|
+
user_styles_dir = File.expand_path("~/.config/vimamsa/styles")
|
|
443
|
+
ssm = GtkSource::StyleSchemeManager.new
|
|
444
|
+
ssm.set_search_path(ssm.search_path << ppath("styles/") << user_styles_dir)
|
|
445
|
+
ssm.get_scheme(VIMAMSA_OVERLAY_SCHEME_ID)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Returns true if the given GtkSource::StyleScheme has a light background.
|
|
449
|
+
# Reads the background color of the "text" style and computes relative luminance.
|
|
450
|
+
def scheme_is_light?(sty)
|
|
451
|
+
text_style = sty.get_style("text")
|
|
452
|
+
return false if text_style.nil?
|
|
453
|
+
bg = text_style.background
|
|
454
|
+
return false if bg.nil? || !bg.start_with?("#")
|
|
455
|
+
hex = bg.delete("#")
|
|
456
|
+
hex = hex.chars.map { |c| c * 2 }.join if hex.length == 3
|
|
457
|
+
return false unless hex.length == 6
|
|
458
|
+
r = hex[0, 2].to_i(16) / 255.0
|
|
459
|
+
g = hex[2, 2].to_i(16) / 255.0
|
|
460
|
+
b = hex[4, 2].to_i(16) / 255.0
|
|
461
|
+
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
462
|
+
luminance > 0.5
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Apply dark/light GTK preference to match the given style scheme.
|
|
466
|
+
def gui_apply_color_mode(sty)
|
|
467
|
+
Gtk::Settings.default.gtk_application_prefer_dark_theme = !scheme_is_light?(sty)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def gui_refresh_style_scheme
|
|
471
|
+
return unless $vmag
|
|
472
|
+
sty = load_vimamsa_scheme
|
|
473
|
+
return if sty.nil?
|
|
474
|
+
gui_apply_color_mode(sty)
|
|
475
|
+
for _k, view in $vmag.buffers
|
|
476
|
+
view.buffer.style_scheme = sty
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
146
480
|
def gui_refresh_font
|
|
147
481
|
return unless $vmag
|
|
148
482
|
provider = Gtk::CssProvider.new
|
|
@@ -45,8 +45,90 @@ class VSourceView < GtkSource::View
|
|
|
45
45
|
@curpos_mark = nil
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Replace each character of every «word with * so the GTK buffer never
|
|
49
|
+
# exposes the real content. Length is preserved → buffer offsets stay valid.
|
|
50
|
+
def mask_for_display(str)
|
|
51
|
+
str.gsub(/(?<=«)\S+/) { |w| "*" * w.length }
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
def set_content(str)
|
|
49
|
-
|
|
55
|
+
if @bufo&.lang == "hyperplaintext"
|
|
56
|
+
self.buffer.set_text(mask_for_display(str))
|
|
57
|
+
else
|
|
58
|
+
self.buffer.set_text(str)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Sync the GTK buffer to mask_for_display(@bufo.to_s).
|
|
63
|
+
# Handles both directions:
|
|
64
|
+
# - masks unmasked «word regions (e.g. after typing a new «)
|
|
65
|
+
# - unmasks orphaned *** spans left behind when « is deleted
|
|
66
|
+
def remask_gtk_buffer
|
|
67
|
+
return unless @bufo&.lang == "hyperplaintext"
|
|
68
|
+
ruby_text = @bufo.to_s
|
|
69
|
+
expected = mask_for_display(ruby_text)
|
|
70
|
+
gtk_text = buffer.text
|
|
71
|
+
return if expected == gtk_text
|
|
72
|
+
return if expected.length != gtk_text.length # sanity check
|
|
73
|
+
|
|
74
|
+
# Find the smallest enclosing differing range and fix it in one edit.
|
|
75
|
+
i = 0
|
|
76
|
+
i += 1 while i < expected.length && expected[i] == gtk_text[i]
|
|
77
|
+
j = expected.length - 1
|
|
78
|
+
j -= 1 while j >= i && expected[j] == gtk_text[j]
|
|
79
|
+
|
|
80
|
+
s_iter = buffer.get_iter_at(:offset => i)
|
|
81
|
+
e_iter = buffer.get_iter_at(:offset => j + 1)
|
|
82
|
+
buffer.delete(s_iter, e_iter)
|
|
83
|
+
buffer.insert(buffer.get_iter_at(:offset => i), expected[i..j])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns [word, word_start, word_end] if +pos+ (Ruby-buffer offset) is
|
|
87
|
+
# inside a «word region, nil otherwise.
|
|
88
|
+
def masked_word_at_cursor(pos)
|
|
89
|
+
text = @bufo.to_s
|
|
90
|
+
return nil unless text.include?("«")
|
|
91
|
+
text.scan(/«(\S+)/) do
|
|
92
|
+
word = $1
|
|
93
|
+
word_start = $~.begin(0) + 1 # first char after «
|
|
94
|
+
word_end = word_start + word.length
|
|
95
|
+
return [word, word_start, word_end] if pos >= word_start && pos <= word_end
|
|
96
|
+
end
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Replace the masked (***) span with the real word from the Ruby buffer.
|
|
101
|
+
def unmask_gtk_region(word, word_start, word_end)
|
|
102
|
+
s_iter = buffer.get_iter_at(:offset => word_start)
|
|
103
|
+
e_iter = buffer.get_iter_at(:offset => word_end)
|
|
104
|
+
buffer.delete(s_iter, e_iter)
|
|
105
|
+
buffer.insert(buffer.get_iter_at(:offset => word_start), word)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# When in insert mode with cursor on a «word, expose the real text in the GTK
|
|
109
|
+
# buffer so the user can see/edit it. Re-mask when cursor leaves or mode changes.
|
|
110
|
+
def update_cursor_unmask
|
|
111
|
+
return unless @bufo&.lang == "hyperplaintext"
|
|
112
|
+
ctype = vma.kbd.get_cursor_type
|
|
113
|
+
pos = @bufo.pos
|
|
114
|
+
|
|
115
|
+
if ctype == :insert
|
|
116
|
+
result = masked_word_at_cursor(pos)
|
|
117
|
+
if result
|
|
118
|
+
word, word_start, word_end = result
|
|
119
|
+
return if @unmasked_range == [word_start, word_end] # already showing this word
|
|
120
|
+
remask_gtk_buffer if @unmasked_range # re-mask previous region
|
|
121
|
+
unmask_gtk_region(word, word_start, word_end)
|
|
122
|
+
@unmasked_range = [word_start, word_end]
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Not insert mode, or cursor is not over a masked word — ensure everything masked
|
|
128
|
+
if @unmasked_range
|
|
129
|
+
@unmasked_range = nil
|
|
130
|
+
remask_gtk_buffer
|
|
131
|
+
end
|
|
50
132
|
end
|
|
51
133
|
|
|
52
134
|
def gutter_width()
|
|
@@ -228,6 +310,7 @@ class VSourceView < GtkSource::View
|
|
|
228
310
|
|
|
229
311
|
click.signal_connect "pressed" do |gesture, n_press, x, y, z|
|
|
230
312
|
debug "SourceView, GestureClick released x=#{x} y=#{y}"
|
|
313
|
+
vma.gui.instance_variable_set(:@kbd_passthrough, false)
|
|
231
314
|
|
|
232
315
|
if buf.visual_mode?
|
|
233
316
|
buf.end_visual_mode
|
|
@@ -484,6 +567,7 @@ class VSourceView < GtkSource::View
|
|
|
484
567
|
end
|
|
485
568
|
end
|
|
486
569
|
if any_change
|
|
570
|
+
remask_gtk_buffer
|
|
487
571
|
#TODO: only when necessary
|
|
488
572
|
self.set_cursor_pos(pos)
|
|
489
573
|
end
|
|
@@ -608,7 +692,7 @@ class VSourceView < GtkSource::View
|
|
|
608
692
|
self.style_context.add_provider(prov)
|
|
609
693
|
@cursor_prov = prov
|
|
610
694
|
end
|
|
611
|
-
@ctype
|
|
695
|
+
@ctype = ctype
|
|
612
696
|
end
|
|
613
697
|
end
|
|
614
698
|
|
|
@@ -666,6 +750,8 @@ class VSourceView < GtkSource::View
|
|
|
666
750
|
self.cursor_visible = false
|
|
667
751
|
self.cursor_visible = true
|
|
668
752
|
end
|
|
753
|
+
|
|
754
|
+
update_cursor_unmask
|
|
669
755
|
end #end draw_cursor
|
|
670
756
|
|
|
671
757
|
CONTEXT_MENU_ITEMS = [
|
|
@@ -691,14 +777,42 @@ class VSourceView < GtkSource::View
|
|
|
691
777
|
end
|
|
692
778
|
end
|
|
693
779
|
end
|
|
780
|
+
|
|
781
|
+
unless vma.gui.app.lookup_action("ctx_open_link")
|
|
782
|
+
act = Gio::SimpleAction.new("ctx_open_link")
|
|
783
|
+
vma.gui.app.add_action(act)
|
|
784
|
+
act.signal_connect("activate") do
|
|
785
|
+
next unless @context_link
|
|
786
|
+
if is_url(@context_link)
|
|
787
|
+
open_url(@context_link)
|
|
788
|
+
else
|
|
789
|
+
fn = hpt_check_cur_word(@context_link)
|
|
790
|
+
open_existing_file(fn) if fn
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
694
795
|
@context_menu = Gtk::PopoverMenu.new
|
|
695
796
|
@context_menu.set_parent(self)
|
|
696
797
|
@context_menu.has_arrow = false
|
|
697
798
|
end
|
|
698
799
|
|
|
800
|
+
def link_at(x, y)
|
|
801
|
+
bx, by = window_to_buffer_coords(:widget, x.to_i, y.to_i)
|
|
802
|
+
iter = get_iter_at_location(bx, by)
|
|
803
|
+
return nil unless iter
|
|
804
|
+
word, _range = @bufo.get_word_in_pos(iter.offset, boundary: :space)
|
|
805
|
+
return nil unless word
|
|
806
|
+
return word if is_url(word)
|
|
807
|
+
return word if word.match?(/⟦.+⟧/)
|
|
808
|
+
nil
|
|
809
|
+
end
|
|
810
|
+
|
|
699
811
|
def show_context_menu(x, y)
|
|
700
812
|
init_context_menu if @context_menu.nil?
|
|
701
813
|
menu = Gio::Menu.new
|
|
814
|
+
@context_link = link_at(x, y)
|
|
815
|
+
menu.append("Open link", "app.ctx_open_link") if @context_link
|
|
702
816
|
CONTEXT_MENU_ITEMS.each { |label, action_id| menu.append(label, "app.ctx_#{action_id}") }
|
|
703
817
|
if @bufo.selection_active?
|
|
704
818
|
CONTEXT_MENU_ITEMS_SELECTION.each { |label, action_id| menu.append(label, "app.ctx_#{action_id}") }
|
data/lib/vimamsa/gui_text.rb
CHANGED
|
@@ -24,26 +24,4 @@ module Gui
|
|
|
24
24
|
self.hilight_range(bf, r, tag: tag)
|
|
25
25
|
}
|
|
26
26
|
end
|
|
27
|
-
|
|
28
|
-
def self.highlight_match_old(bf, str, color: "#aa0000ff")
|
|
29
|
-
vbuf = bf.view.buffer
|
|
30
|
-
r = Regexp.new(Regexp.escape(str), Regexp::IGNORECASE)
|
|
31
|
-
|
|
32
|
-
hlparts = []
|
|
33
|
-
|
|
34
|
-
tt = vma.gui.view.buffer.tag_table.lookup("highlight_match_tag")
|
|
35
|
-
if tt.nil?
|
|
36
|
-
tt = vma.gui.view.buffer.create_tag("highlight_match_tag")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
tt.weight = 650
|
|
40
|
-
tt.foreground = color
|
|
41
|
-
|
|
42
|
-
ind = scan_indexes(bf, r)
|
|
43
|
-
ind.each { |x|
|
|
44
|
-
itr = vbuf.get_iter_at(:offset => x)
|
|
45
|
-
itr2 = vbuf.get_iter_at(:offset => x + str.size)
|
|
46
|
-
vbuf.apply_tag(tt, itr, itr2)
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
27
|
end
|