ruvim 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.
@@ -1,5 +1,7 @@
1
1
  module RuVim
2
2
  class ConfigLoader
3
+ SAFE_FILETYPE_RE = /\A[a-zA-Z0-9_+-]+\z/.freeze
4
+
3
5
  def initialize(command_registry:, ex_registry:, keymaps:, command_host:)
4
6
  @command_registry = command_registry
5
7
  @ex_registry = ex_registry
@@ -32,6 +34,7 @@ module RuVim
32
34
  filetype = buffer.options["filetype"].to_s
33
35
  return nil if filetype.empty?
34
36
  return nil if buffer.options["__ftplugin_loaded__"] == filetype
37
+ return nil unless safe_filetype_name?(filetype)
35
38
 
36
39
  path = ftplugin_path_for(filetype)
37
40
  return nil unless path && File.file?(path)
@@ -58,11 +61,22 @@ module RuVim
58
61
 
59
62
  def xdg_ftplugin_path(filetype)
60
63
  base = ::ENV["XDG_CONFIG_HOME"]
61
- if base && !base.empty?
62
- File.join(base, "ruvim", "ftplugin", "#{filetype}.rb")
63
- else
64
- File.expand_path("~/.config/ruvim/ftplugin/#{filetype}.rb")
65
- end
64
+ root =
65
+ if base && !base.empty?
66
+ File.join(base, "ruvim", "ftplugin")
67
+ else
68
+ File.expand_path("~/.config/ruvim/ftplugin")
69
+ end
70
+
71
+ candidate = File.expand_path(File.join(root, "#{filetype}.rb"))
72
+ root_prefix = File.join(File.expand_path(root), "")
73
+ return nil unless candidate.start_with?(root_prefix)
74
+
75
+ candidate
76
+ end
77
+
78
+ def safe_filetype_name?(filetype)
79
+ SAFE_FILETYPE_RE.match?(filetype.to_s)
66
80
  end
67
81
  end
68
82
  end
data/lib/ruvim/context.rb CHANGED
@@ -15,12 +15,5 @@ module RuVim
15
15
  editor.current_buffer
16
16
  end
17
17
 
18
- def count
19
- invocation&.count || 1
20
- end
21
-
22
- def bang?
23
- invocation&.bang || false
24
- end
25
18
  end
26
19
  end
@@ -19,6 +19,16 @@ module RuVim
19
19
  end
20
20
 
21
21
  def dispatch_ex(editor, line)
22
+ raw = line.to_s.strip
23
+ if raw.start_with?("!")
24
+ command = raw[1..].to_s.strip
25
+ invocation = CommandInvocation.new(id: "__shell__", argv: [command])
26
+ ctx = Context.new(editor:, invocation:)
27
+ @command_host.ex_shell(ctx, command:)
28
+ editor.enter_normal_mode
29
+ return
30
+ end
31
+
22
32
  if (sub = parse_global_substitute(line))
23
33
  invocation = CommandInvocation.new(id: "__substitute__", kwargs: sub)
24
34
  ctx = Context.new(editor:, invocation:)
@@ -11,6 +11,25 @@ module RuVim
11
11
  end
12
12
 
13
13
  code = ch.ord
14
+ return cached_codepoint_width(code) if codepoint_cacheable?(code)
15
+
16
+ uncached_codepoint_width(code)
17
+ end
18
+
19
+ def codepoint_cacheable?(code)
20
+ !code.nil? && !code.zero?
21
+ end
22
+
23
+ def cached_codepoint_width(code)
24
+ aw = ambiguous_width
25
+ @codepoint_width_cache ||= {}
26
+ key = [code, aw]
27
+ return @codepoint_width_cache[key] if @codepoint_width_cache.key?(key)
28
+
29
+ @codepoint_width_cache[key] = uncached_codepoint_width(code)
30
+ end
31
+
32
+ def uncached_codepoint_width(code)
14
33
  return 0 if code.zero?
15
34
  return 0 if combining_mark?(code)
16
35
  return 0 if zero_width_codepoint?(code)
@@ -102,9 +121,13 @@ module RuVim
102
121
 
103
122
  def ambiguous_width
104
123
  env = ::ENV["RUVIM_AMBIGUOUS_WIDTH"]
105
- return 2 if env == "2"
124
+ if !defined?(@ambiguous_width_cached) || @ambiguous_width_env != env
125
+ @ambiguous_width_env = env
126
+ @ambiguous_width_cached = (env == "2" ? 2 : 1)
127
+ @codepoint_width_cache = {}
128
+ end
106
129
 
107
- 1
130
+ @ambiguous_width_cached
108
131
  end
109
132
  end
110
133
  end
data/lib/ruvim/editor.rb CHANGED
@@ -3,15 +3,57 @@ module RuVim
3
3
  OPTION_DEFS = {
4
4
  "number" => { default_scope: :window, type: :bool, default: false },
5
5
  "relativenumber" => { default_scope: :window, type: :bool, default: false },
6
+ "wrap" => { default_scope: :window, type: :bool, default: true },
7
+ "linebreak" => { default_scope: :window, type: :bool, default: false },
8
+ "breakindent" => { default_scope: :window, type: :bool, default: false },
9
+ "cursorline" => { default_scope: :window, type: :bool, default: false },
10
+ "scrolloff" => { default_scope: :window, type: :int, default: 0 },
11
+ "sidescrolloff" => { default_scope: :window, type: :int, default: 0 },
12
+ "numberwidth" => { default_scope: :window, type: :int, default: 4 },
13
+ "colorcolumn" => { default_scope: :window, type: :string, default: nil },
14
+ "signcolumn" => { default_scope: :window, type: :string, default: "auto" },
15
+ "list" => { default_scope: :window, type: :bool, default: false },
16
+ "listchars" => { default_scope: :window, type: :string, default: "tab:>-,trail:-,nbsp:+" },
17
+ "showbreak" => { default_scope: :window, type: :string, default: ">" },
18
+ "showmatch" => { default_scope: :global, type: :bool, default: false },
19
+ "matchtime" => { default_scope: :global, type: :int, default: 5 },
20
+ "whichwrap" => { default_scope: :global, type: :string, default: "" },
21
+ "virtualedit" => { default_scope: :global, type: :string, default: "" },
6
22
  "ignorecase" => { default_scope: :global, type: :bool, default: false },
7
23
  "smartcase" => { default_scope: :global, type: :bool, default: false },
8
24
  "hlsearch" => { default_scope: :global, type: :bool, default: true },
25
+ "incsearch" => { default_scope: :global, type: :bool, default: false },
26
+ "splitbelow" => { default_scope: :global, type: :bool, default: false },
27
+ "splitright" => { default_scope: :global, type: :bool, default: false },
28
+ "hidden" => { default_scope: :global, type: :bool, default: false },
29
+ "autowrite" => { default_scope: :global, type: :bool, default: false },
30
+ "clipboard" => { default_scope: :global, type: :string, default: "" },
31
+ "timeoutlen" => { default_scope: :global, type: :int, default: 1000 },
32
+ "ttimeoutlen" => { default_scope: :global, type: :int, default: 50 },
33
+ "backspace" => { default_scope: :global, type: :string, default: "indent,eol,start" },
34
+ "completeopt" => { default_scope: :global, type: :string, default: "menu,menuone,noselect" },
35
+ "pumheight" => { default_scope: :global, type: :int, default: 10 },
36
+ "wildmode" => { default_scope: :global, type: :string, default: "full" },
37
+ "wildignore" => { default_scope: :global, type: :string, default: "" },
38
+ "wildignorecase" => { default_scope: :global, type: :bool, default: false },
39
+ "wildmenu" => { default_scope: :global, type: :bool, default: false },
40
+ "termguicolors" => { default_scope: :global, type: :bool, default: false },
41
+ "path" => { default_scope: :buffer, type: :string, default: nil },
42
+ "suffixesadd" => { default_scope: :buffer, type: :string, default: nil },
43
+ "textwidth" => { default_scope: :buffer, type: :int, default: 0 },
44
+ "formatoptions" => { default_scope: :buffer, type: :string, default: nil },
45
+ "expandtab" => { default_scope: :buffer, type: :bool, default: false },
46
+ "shiftwidth" => { default_scope: :buffer, type: :int, default: 2 },
47
+ "softtabstop" => { default_scope: :buffer, type: :int, default: 0 },
48
+ "autoindent" => { default_scope: :buffer, type: :bool, default: false },
49
+ "smartindent" => { default_scope: :buffer, type: :bool, default: false },
50
+ "iskeyword" => { default_scope: :buffer, type: :string, default: nil },
9
51
  "tabstop" => { default_scope: :buffer, type: :int, default: 2 },
10
52
  "filetype" => { default_scope: :buffer, type: :string, default: nil }
11
53
  }.freeze
12
54
 
13
55
  attr_reader :buffers, :windows
14
- attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :window_layout, :restricted_mode
56
+ attr_accessor :current_window_id, :mode, :message, :pending_count, :alternate_buffer_id, :window_layout, :restricted_mode, :current_window_view_height_hint
15
57
 
16
58
  def initialize
17
59
  @buffers = {}
@@ -29,8 +71,10 @@ module RuVim
29
71
  @window_layout = :single
30
72
  @message = ""
31
73
  @message_kind = :info
74
+ @message_deadline = nil
32
75
  @pending_count = nil
33
76
  @restricted_mode = false
77
+ @current_window_view_height_hint = 1
34
78
  @running = true
35
79
  @global_options = default_global_options
36
80
  @command_line = CommandLine.new
@@ -201,6 +245,12 @@ module RuVim
201
245
 
202
246
  payload = write_register_payload(key, text: text.to_s, type: type.to_sym)
203
247
  write_clipboard_register(key, payload)
248
+ if key == "\""
249
+ if (default_clip = clipboard_default_register_key)
250
+ mirror = write_register_payload(default_clip, text: payload[:text], type: payload[:type])
251
+ write_clipboard_register(default_clip, mirror)
252
+ end
253
+ end
204
254
  @registers["\""] = payload unless key == "\""
205
255
  payload
206
256
  end
@@ -225,6 +275,14 @@ module RuVim
225
275
 
226
276
  def get_register(name = "\"")
227
277
  key = name.to_s.downcase
278
+ if key == "\""
279
+ if (default_clip = clipboard_default_register_key)
280
+ if (payload = read_clipboard_register(default_clip))
281
+ @registers["\""] = dup_register_payload(payload)
282
+ return @registers["\""]
283
+ end
284
+ end
285
+ end
228
286
  return read_clipboard_register(key) if clipboard_register?(key)
229
287
 
230
288
  @registers[key]
@@ -458,10 +516,16 @@ module RuVim
458
516
  window
459
517
  end
460
518
 
461
- def split_current_window(layout: :horizontal)
519
+ def split_current_window(layout: :horizontal, place: :after)
462
520
  save_current_tabpage_state! unless @suspend_tab_autosave
463
521
  src = current_window
464
- win = add_window(buffer_id: src.buffer_id)
522
+ id = next_window_id
523
+ win = Window.new(id:, buffer_id: src.buffer_id)
524
+ @windows[id] = win
525
+ src_idx = @window_order.index(src.id) || (@window_order.length - 1)
526
+ insert_idx = (place.to_sym == :before ? src_idx : src_idx + 1)
527
+ @window_order.insert(insert_idx, win.id)
528
+ ensure_initial_tabpage!
465
529
  win.cursor_x = src.cursor_x
466
530
  win.cursor_y = src.cursor_y
467
531
  win.row_offset = src.row_offset
@@ -641,6 +705,45 @@ module RuVim
641
705
  ids[(idx + step) % ids.length]
642
706
  end
643
707
 
708
+ def delete_buffer(buffer_id)
709
+ id = buffer_id.to_i
710
+ buffer = @buffers[id]
711
+ return nil unless buffer
712
+
713
+ if @buffers.length <= 1
714
+ replacement = add_empty_buffer
715
+ else
716
+ replacement = nil
717
+ end
718
+
719
+ fallback_id =
720
+ if replacement
721
+ replacement.id
722
+ else
723
+ candidates = @buffers.keys.reject { |bid| bid == id }
724
+ alt = @alternate_buffer_id
725
+ (alt && alt != id && @buffers.key?(alt)) ? alt : candidates.first
726
+ end
727
+
728
+ @windows.each_value do |win|
729
+ next unless win.buffer_id == id
730
+ next unless fallback_id
731
+
732
+ win.buffer_id = fallback_id
733
+ win.cursor_x = 0
734
+ win.cursor_y = 0
735
+ win.row_offset = 0
736
+ win.col_offset = 0
737
+ end
738
+
739
+ @buffers.delete(id)
740
+ @local_marks.delete(id)
741
+ @alternate_buffer_id = nil if @alternate_buffer_id == id
742
+ save_current_tabpage_state! unless @suspend_tab_autosave
743
+ ensure_bootstrap_buffer! if @buffers.empty?
744
+ true
745
+ end
746
+
644
747
  def window_order
645
748
  @window_order
646
749
  end
@@ -692,6 +795,15 @@ module RuVim
692
795
  current_quickfix_item
693
796
  end
694
797
 
798
+ def select_quickfix(index)
799
+ items = @quickfix_list[:items]
800
+ return nil if items.empty?
801
+
802
+ i = [[index.to_i, 0].max, items.length - 1].min
803
+ @quickfix_list[:index] = i
804
+ current_quickfix_item
805
+ end
806
+
695
807
  def location_list(window_id = current_window_id)
696
808
  @location_lists[window_id]
697
809
  end
@@ -721,6 +833,16 @@ module RuVim
721
833
  current_location_list_item(window_id)
722
834
  end
723
835
 
836
+ def select_location_list(index, window_id: current_window_id)
837
+ list = location_list(window_id)
838
+ items = list[:items]
839
+ return nil if items.empty?
840
+
841
+ i = [[index.to_i, 0].max, items.length - 1].min
842
+ list[:index] = i
843
+ current_location_list_item(window_id)
844
+ end
845
+
724
846
  def find_window_ids_by_buffer_kind(kind)
725
847
  sym = kind.to_sym
726
848
  @window_order.select do |wid|
@@ -785,24 +907,62 @@ module RuVim
785
907
  def echo(msg)
786
908
  @message_kind = :info
787
909
  @message = msg.to_s
910
+ @message_deadline = nil
911
+ end
912
+
913
+ def echo_temporary(msg, duration_seconds:)
914
+ @message_kind = :info
915
+ @message = msg.to_s
916
+ dur = duration_seconds.to_f
917
+ @message_deadline = dur.positive? ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + dur) : nil
918
+ rescue StandardError
919
+ @message_deadline = nil
788
920
  end
789
921
 
790
922
  def echo_error(msg)
791
923
  @message_kind = :error
792
924
  @message = msg.to_s
925
+ @message_deadline = nil
793
926
  end
794
927
 
795
928
  def clear_message
796
929
  @message_kind = :info
797
930
  @message = ""
931
+ @message_deadline = nil
798
932
  end
799
933
 
800
934
  def message_error?
801
935
  !@message.to_s.empty? && @message_kind == :error
802
936
  end
803
937
 
938
+ def transient_message_timeout_seconds(now: nil)
939
+ return nil unless @message_deadline
940
+ return nil if message_error?
941
+ return nil if command_line_active?
942
+
943
+ now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
944
+ [@message_deadline - now, 0.0].max
945
+ rescue StandardError
946
+ nil
947
+ end
948
+
949
+ def clear_expired_transient_message!(now: nil)
950
+ return false unless @message_deadline
951
+ return false if message_error?
952
+ return false if command_line_active?
953
+
954
+ now ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
955
+ return false if now < @message_deadline
956
+
957
+ clear_message
958
+ true
959
+ rescue StandardError
960
+ false
961
+ end
962
+
804
963
  def text_viewport_size(rows:, cols:)
805
- text_rows = command_area_active? ? rows - 2 : rows - 1
964
+ # Reserve one status row + one command/error row at the bottom.
965
+ text_rows = rows - 2
806
966
  [text_rows, cols]
807
967
  end
808
968
 
@@ -950,6 +1110,15 @@ module RuVim
950
1110
  key == "+" || key == "*"
951
1111
  end
952
1112
 
1113
+ def clipboard_default_register_key
1114
+ spec = @global_options["clipboard"].to_s
1115
+ parts = spec.split(",").map { |s| s.strip.downcase }.reject(&:empty?)
1116
+ return "+" if parts.include?("unnamedplus")
1117
+ return "*" if parts.include?("unnamed")
1118
+
1119
+ nil
1120
+ end
1121
+
953
1122
  def write_clipboard_register(key, payload)
954
1123
  return unless clipboard_register?(key.downcase)
955
1124