fatty 0.99.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 +7 -0
- data/.envrc +2 -0
- data/.simplecov +23 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +34 -0
- data/CHANGELOG.org +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/README.org +166 -0
- data/Rakefile +15 -0
- data/TODO.org +163 -0
- data/examples/markdown/native-markdown.md +370 -0
- data/examples/markdown/ox-gfm-markdown.md +373 -0
- data/examples/markdown/ox-gfm-markdown.org +376 -0
- data/exe/fatty +275 -0
- data/fatty.gemspec +42 -0
- data/lib/fatty/accept_env.rb +32 -0
- data/lib/fatty/action.rb +103 -0
- data/lib/fatty/action_environment.rb +42 -0
- data/lib/fatty/actionable.rb +73 -0
- data/lib/fatty/alert.rb +93 -0
- data/lib/fatty/ansi/renderer.rb +168 -0
- data/lib/fatty/ansi.rb +352 -0
- data/lib/fatty/colors/color.rb +379 -0
- data/lib/fatty/colors/pairs.rb +73 -0
- data/lib/fatty/colors/palette.rb +73 -0
- data/lib/fatty/colors/rgb.txt +788 -0
- data/lib/fatty/colors.rb +5 -0
- data/lib/fatty/config.rb +86 -0
- data/lib/fatty/config_files/config.yml +50 -0
- data/lib/fatty/config_files/help.md +120 -0
- data/lib/fatty/config_files/help.org +124 -0
- data/lib/fatty/config_files/keybindings.yml +49 -0
- data/lib/fatty/config_files/keydefs.yml +23 -0
- data/lib/fatty/config_files/themes/mono.yml +76 -0
- data/lib/fatty/config_files/themes/nordic.yml +77 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
- data/lib/fatty/config_files/themes/terminal.yml +90 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
- data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
- data/lib/fatty/core_ext/string.rb +21 -0
- data/lib/fatty/core_ext.rb +3 -0
- data/lib/fatty/counter.rb +81 -0
- data/lib/fatty/curses/context.rb +279 -0
- data/lib/fatty/curses/curses_coder.rb +684 -0
- data/lib/fatty/curses/event_source.rb +230 -0
- data/lib/fatty/curses/key_decoder.rb +183 -0
- data/lib/fatty/curses/patch.rb +116 -0
- data/lib/fatty/curses/window_styling.rb +32 -0
- data/lib/fatty/curses.rb +16 -0
- data/lib/fatty/env.rb +100 -0
- data/lib/fatty/help.rb +41 -0
- data/lib/fatty/history/entry.rb +71 -0
- data/lib/fatty/history.rb +289 -0
- data/lib/fatty/input_buffer.rb +998 -0
- data/lib/fatty/input_field.rb +507 -0
- data/lib/fatty/key_event.rb +342 -0
- data/lib/fatty/key_map.rb +392 -0
- data/lib/fatty/keymaps/emacs.rb +189 -0
- data/lib/fatty/log_formats/json.rb +47 -0
- data/lib/fatty/log_formats/text.rb +67 -0
- data/lib/fatty/logger.rb +142 -0
- data/lib/fatty/markdown/ansi_renderer.rb +373 -0
- data/lib/fatty/markdown/render.rb +22 -0
- data/lib/fatty/markdown.rb +4 -0
- data/lib/fatty/menu_env.rb +22 -0
- data/lib/fatty/mouse_event.rb +32 -0
- data/lib/fatty/output_buffer.rb +78 -0
- data/lib/fatty/pager.rb +801 -0
- data/lib/fatty/prompt.rb +40 -0
- data/lib/fatty/renderer/curses.rb +697 -0
- data/lib/fatty/renderer/truecolor.rb +607 -0
- data/lib/fatty/renderer.rb +419 -0
- data/lib/fatty/screen.rb +96 -0
- data/lib/fatty/search.rb +43 -0
- data/lib/fatty/session/alert_session.rb +52 -0
- data/lib/fatty/session/input_session.rb +99 -0
- data/lib/fatty/session/isearch_session.rb +172 -0
- data/lib/fatty/session/keytest_session.rb +236 -0
- data/lib/fatty/session/modal_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +105 -0
- data/lib/fatty/session/popup_session.rb +540 -0
- data/lib/fatty/session/prompt_session.rb +157 -0
- data/lib/fatty/session/search_session.rb +136 -0
- data/lib/fatty/session/shell_session.rb +566 -0
- data/lib/fatty/session.rb +173 -0
- data/lib/fatty/sessions.rb +14 -0
- data/lib/fatty/terminal/popup_owner.rb +26 -0
- data/lib/fatty/terminal/progress.rb +374 -0
- data/lib/fatty/terminal.rb +1067 -0
- data/lib/fatty/themes/loader.rb +136 -0
- data/lib/fatty/themes/manager.rb +71 -0
- data/lib/fatty/themes/registry.rb +64 -0
- data/lib/fatty/themes/resolver.rb +224 -0
- data/lib/fatty/themes/themes.rb +131 -0
- data/lib/fatty/themes.rb +6 -0
- data/lib/fatty/version.rb +5 -0
- data/lib/fatty/view/alert_view.rb +14 -0
- data/lib/fatty/view/cursor_view.rb +18 -0
- data/lib/fatty/view/input_view.rb +9 -0
- data/lib/fatty/view/output_view.rb +9 -0
- data/lib/fatty/view/status_view.rb +14 -0
- data/lib/fatty/view.rb +33 -0
- data/lib/fatty/viewport.rb +90 -0
- data/lib/fatty/views.rb +9 -0
- data/lib/fatty.rb +55 -0
- data/sig/fatty.rbs +4 -0
- metadata +250 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "strscan"
|
|
5
|
+
|
|
6
|
+
module Fatty
|
|
7
|
+
# The =InputField= class is a thin controller around an InputBuffer that
|
|
8
|
+
# adds:
|
|
9
|
+
# - A prompt (text shown before the editable buffer)
|
|
10
|
+
# - Optional command history integration (previous/next)
|
|
11
|
+
# - A small set of "editor actions" intended to be bound to keys
|
|
12
|
+
#
|
|
13
|
+
# =InputField= intentionally does *not* perform terminal I/O or rendering.
|
|
14
|
+
# Higher-level UI components (e.g., Terminal/Screen/widgets) are responsible
|
|
15
|
+
# for:
|
|
16
|
+
#
|
|
17
|
+
# - Decoding keys and dispatching actions
|
|
18
|
+
# - Translating buffer/cursor state into screen coordinates
|
|
19
|
+
# - Drawing the prompt + buffer text and placing the cursor
|
|
20
|
+
#
|
|
21
|
+
# Word-motion and word-deletion semantics are delegated to InputBuffer so
|
|
22
|
+
# the definition of "word" can be configured in one place (via
|
|
23
|
+
# =InputBuffer='s word_chars/word_re).
|
|
24
|
+
class InputField
|
|
25
|
+
include Actionable
|
|
26
|
+
|
|
27
|
+
attr_reader :buffer, :prompt, :history
|
|
28
|
+
|
|
29
|
+
def initialize(
|
|
30
|
+
prompt:,
|
|
31
|
+
buffer: nil,
|
|
32
|
+
completion_proc: nil,
|
|
33
|
+
history: nil,
|
|
34
|
+
history_kind: :command,
|
|
35
|
+
history_ctx: nil
|
|
36
|
+
)
|
|
37
|
+
@prompt = Prompt.ensure(prompt)
|
|
38
|
+
@history = history
|
|
39
|
+
@history_kind = history_kind
|
|
40
|
+
@history_ctx = history_ctx
|
|
41
|
+
@completion_proc = completion_proc
|
|
42
|
+
@completion_cycle_base = nil
|
|
43
|
+
@completion_cycle_candidates = []
|
|
44
|
+
@completion_cycle_index = nil
|
|
45
|
+
|
|
46
|
+
@buffer =
|
|
47
|
+
if buffer
|
|
48
|
+
buffer
|
|
49
|
+
else
|
|
50
|
+
cfg = Fatty::Config.config
|
|
51
|
+
word_chars = cfg.dig(:input_buffer, :word_chars) || Fatty::InputBuffer::DEFAULT_WORD_CHARS
|
|
52
|
+
Fatty::InputBuffer.new(word_chars: word_chars)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# :category: Inspect
|
|
57
|
+
|
|
58
|
+
def to_s
|
|
59
|
+
"<InputField:#{object_id}> Prompt => #{prompt} Buffer => #{buffer}"
|
|
60
|
+
end
|
|
61
|
+
alias_method :inspect, :to_s
|
|
62
|
+
|
|
63
|
+
# :category: Queries
|
|
64
|
+
|
|
65
|
+
def empty?
|
|
66
|
+
buffer.text == ""
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Visual cursor X position in the window
|
|
70
|
+
def cursor_x
|
|
71
|
+
before_cursor = buffer.text.to_s[0...buffer.cursor].to_s
|
|
72
|
+
prompt_width + Fatty::Ansi.visible_length(before_cursor)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def prompt_text
|
|
76
|
+
prompt.text
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The prompt might use coloring, so we use the visible length stripped of
|
|
80
|
+
# ANSI controls.
|
|
81
|
+
def prompt_width
|
|
82
|
+
Fatty::Ansi.visible_length(prompt_text.to_s.lines.last.to_s)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def snapshot_input_state
|
|
86
|
+
[
|
|
87
|
+
prompt_text.to_s.dup.freeze,
|
|
88
|
+
buffer.text.to_s.dup.freeze,
|
|
89
|
+
buffer.virtual_suffix.to_s.dup.freeze,
|
|
90
|
+
cursor_x,
|
|
91
|
+
(r = buffer.region_range) ? [r.begin, r.end] : nil,
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# :category: Setters
|
|
96
|
+
|
|
97
|
+
def prompt=(prompt)
|
|
98
|
+
@prompt = Prompt.ensure(prompt)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :category: Actions
|
|
102
|
+
|
|
103
|
+
desc "Accept the current line, add to history, and clear the buffer"
|
|
104
|
+
action :accept_line do
|
|
105
|
+
line = buffer.text.dup
|
|
106
|
+
if history
|
|
107
|
+
history.add(
|
|
108
|
+
line,
|
|
109
|
+
kind: resolve_history_kind,
|
|
110
|
+
ctx: resolve_history_ctx,
|
|
111
|
+
)
|
|
112
|
+
buffer.clear
|
|
113
|
+
line
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
desc "Replace buffer with the previous history entry"
|
|
118
|
+
action :history_prev do
|
|
119
|
+
return unless history
|
|
120
|
+
|
|
121
|
+
buffer.replace(history.previous_for(resolve_history_kind, current: buffer.text, ctx: resolve_history_ctx))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
desc "Replace buffer with the next history entry"
|
|
125
|
+
action :history_next do
|
|
126
|
+
return unless history
|
|
127
|
+
|
|
128
|
+
buffer.replace(history.next_for(resolve_history_kind, ctx: resolve_history_ctx))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
desc "Paste text into the field, normalizing to one line"
|
|
132
|
+
action :paste do |str|
|
|
133
|
+
s = str.to_s
|
|
134
|
+
s = s.gsub(/\r\n?/, "\n")
|
|
135
|
+
s = s.tr("\n", " ")
|
|
136
|
+
buffer.insert(s)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def act_on(action, *args, env: nil, **kwargs)
|
|
140
|
+
return unless action
|
|
141
|
+
|
|
142
|
+
reset_history_cursor_for(action)
|
|
143
|
+
|
|
144
|
+
if Fatty::Actions.registered?(action)
|
|
145
|
+
if env
|
|
146
|
+
Fatty::Actions.call(action, env, *args, **kwargs)
|
|
147
|
+
else
|
|
148
|
+
defn = Fatty::Actions.lookup(action)
|
|
149
|
+
target =
|
|
150
|
+
case defn[:on]
|
|
151
|
+
when :field then self
|
|
152
|
+
when :buffer then buffer
|
|
153
|
+
else
|
|
154
|
+
raise Fatty::ActionError, "Cannot dispatch #{action} without env for target #{defn[:on].inspect}"
|
|
155
|
+
end
|
|
156
|
+
target.public_send(defn[:method], *args, **kwargs)
|
|
157
|
+
end
|
|
158
|
+
elsif buffer.respond_to?(action)
|
|
159
|
+
buffer.public_send(action, *args, **kwargs)
|
|
160
|
+
elsif respond_to?(action)
|
|
161
|
+
public_send(action, *args, **kwargs)
|
|
162
|
+
else
|
|
163
|
+
raise Fatty::ActionError, "Unknown action: #{action}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# :category: Completion
|
|
168
|
+
|
|
169
|
+
def autosuggestion
|
|
170
|
+
return if buffer.text.empty?
|
|
171
|
+
|
|
172
|
+
active_completion_autosuggestion || default_completion_autosuggestion || history_autosuggestion
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def completion_candidates
|
|
176
|
+
return [] unless @completion_proc
|
|
177
|
+
|
|
178
|
+
prefix = buffer.completion_prefix.to_s
|
|
179
|
+
return [] if prefix.empty?
|
|
180
|
+
|
|
181
|
+
Array(@completion_proc.call(buffer))
|
|
182
|
+
.compact
|
|
183
|
+
.map(&:to_s)
|
|
184
|
+
.reject(&:empty?)
|
|
185
|
+
.select { |s| s.start_with?(prefix) }
|
|
186
|
+
.reject { |s| s == prefix }
|
|
187
|
+
.uniq
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def completion_suggestions
|
|
191
|
+
raw =
|
|
192
|
+
if path_completion_candidates.any?
|
|
193
|
+
path_completion_candidates
|
|
194
|
+
else
|
|
195
|
+
completion_candidates
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
raw
|
|
199
|
+
.map { |candidate| build_line_with_completion(candidate) }
|
|
200
|
+
.reject { |line| line == buffer.text.to_s }
|
|
201
|
+
.uniq
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def default_completion_autosuggestion
|
|
205
|
+
suggestions = completion_suggestions
|
|
206
|
+
result = suggestions.first
|
|
207
|
+
result
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def active_completion_autosuggestion
|
|
211
|
+
text = buffer.text.to_s
|
|
212
|
+
result = nil
|
|
213
|
+
|
|
214
|
+
if @completion_cycle_base == text &&
|
|
215
|
+
@completion_cycle_index &&
|
|
216
|
+
!@completion_cycle_candidates.empty?
|
|
217
|
+
result = @completion_cycle_candidates[@completion_cycle_index]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
result
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def cycle_completion!
|
|
224
|
+
text = buffer.text.to_s
|
|
225
|
+
suggestions = completion_suggestions
|
|
226
|
+
result = nil
|
|
227
|
+
|
|
228
|
+
if suggestions.empty?
|
|
229
|
+
reset_completion_cycle!
|
|
230
|
+
elsif @completion_cycle_base == text &&
|
|
231
|
+
@completion_cycle_candidates == suggestions &&
|
|
232
|
+
@completion_cycle_index
|
|
233
|
+
@completion_cycle_index = (@completion_cycle_index + 1) % @completion_cycle_candidates.length
|
|
234
|
+
result = @completion_cycle_candidates[@completion_cycle_index]
|
|
235
|
+
else
|
|
236
|
+
@completion_cycle_base = text
|
|
237
|
+
@completion_cycle_candidates = suggestions
|
|
238
|
+
@completion_cycle_index =
|
|
239
|
+
if suggestions.length > 1
|
|
240
|
+
1
|
|
241
|
+
else
|
|
242
|
+
0
|
|
243
|
+
end
|
|
244
|
+
result = @completion_cycle_candidates[@completion_cycle_index]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sync_virtual_suffix!
|
|
248
|
+
result
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def reset_completion_cycle!
|
|
252
|
+
@completion_cycle_base = nil
|
|
253
|
+
@completion_cycle_candidates = []
|
|
254
|
+
@completion_cycle_index = nil
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def history_autosuggestion
|
|
259
|
+
return if history.nil?
|
|
260
|
+
|
|
261
|
+
history.suggest_for(
|
|
262
|
+
resolve_history_kind,
|
|
263
|
+
prefix: buffer.text,
|
|
264
|
+
ctx: resolve_history_ctx,
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def path_completion_candidates
|
|
269
|
+
prefix = path_completion_prefix
|
|
270
|
+
return [] if prefix.nil? || prefix.empty?
|
|
271
|
+
|
|
272
|
+
rendered_path_candidates(prefix)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def path_like_prefix?(prefix)
|
|
276
|
+
s = prefix.to_s
|
|
277
|
+
return false if s.empty?
|
|
278
|
+
|
|
279
|
+
s.start_with?("/", "~/", "./", "../") || s.include?("/")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def path_completion_prefix
|
|
283
|
+
r = path_completion_range
|
|
284
|
+
return if r.nil?
|
|
285
|
+
|
|
286
|
+
buffer.text[r].to_s
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Use a StringScanner to determine where a pathname occurs before the
|
|
290
|
+
# cursor for purposes of providing completions. It considers escaped
|
|
291
|
+
# characters, including escaped whitespace as part of a plausible
|
|
292
|
+
# pathname.
|
|
293
|
+
def path_completion_range
|
|
294
|
+
text = buffer.text.to_s
|
|
295
|
+
cur = buffer.cursor
|
|
296
|
+
return if cur < 0
|
|
297
|
+
|
|
298
|
+
before = text[0...cur]
|
|
299
|
+
scanner = StringScanner.new(before)
|
|
300
|
+
|
|
301
|
+
last_token = nil
|
|
302
|
+
until scanner.eos?
|
|
303
|
+
next if scanner.scan(/\s+/)
|
|
304
|
+
|
|
305
|
+
if (token = scanner.scan(/(?:\\.|[^\s\\])+/))
|
|
306
|
+
last_token = [scanner.pos - token.length, scanner.pos]
|
|
307
|
+
else
|
|
308
|
+
scanner.getch
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
return unless last_token
|
|
313
|
+
|
|
314
|
+
start_i, end_i = last_token
|
|
315
|
+
prefix = before[start_i...end_i]
|
|
316
|
+
|
|
317
|
+
return unless path_like_prefix?(unescape_path(prefix))
|
|
318
|
+
|
|
319
|
+
start_i...end_i
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def rendered_path_candidates(prefix)
|
|
323
|
+
raw_prefix = unescape_path(prefix)
|
|
324
|
+
expanded = expand_path_prefix(raw_prefix)
|
|
325
|
+
return [] if expanded.nil? || expanded.empty?
|
|
326
|
+
|
|
327
|
+
if File.directory?(expanded) && !raw_prefix.end_with?("/")
|
|
328
|
+
return [escape_path("#{raw_prefix}/")]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if raw_prefix.end_with?("/")
|
|
332
|
+
dir_part = expanded
|
|
333
|
+
base_part = ""
|
|
334
|
+
else
|
|
335
|
+
dir_part = File.dirname(expanded)
|
|
336
|
+
base_part = File.basename(expanded)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
dir_part = "." if dir_part.nil? || dir_part.empty?
|
|
340
|
+
return [] unless Dir.exist?(dir_part)
|
|
341
|
+
|
|
342
|
+
# Sort normal directories, normal files, hidden directories, then hidden
|
|
343
|
+
# files, but not if the prefix starts with '.' or '#', then treat hidden
|
|
344
|
+
# files normally.
|
|
345
|
+
entries =
|
|
346
|
+
Dir.children(dir_part)
|
|
347
|
+
.select { |name| name.start_with?(base_part) }
|
|
348
|
+
.sort_by do |name|
|
|
349
|
+
full = File.join(dir_part, name)
|
|
350
|
+
hide_penalty =
|
|
351
|
+
if base_part.match?(/\A[.#]/)
|
|
352
|
+
0
|
|
353
|
+
else
|
|
354
|
+
name.match?(/\A[.#]/) ? 1 : 0
|
|
355
|
+
end
|
|
356
|
+
file_penalty = File.directory?(full) ? 0 : 1
|
|
357
|
+
[hide_penalty, file_penalty, name]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
return [] if entries.empty?
|
|
361
|
+
|
|
362
|
+
entries.map do |chosen|
|
|
363
|
+
full = File.join(dir_part, chosen)
|
|
364
|
+
|
|
365
|
+
rendered =
|
|
366
|
+
if raw_prefix.start_with?("~/")
|
|
367
|
+
File.join("~", full.delete_prefix("#{Dir.home}/"))
|
|
368
|
+
elsif raw_prefix.start_with?("./") && dir_part == "."
|
|
369
|
+
"./#{chosen}"
|
|
370
|
+
elsif raw_prefix.start_with?("../") && dir_part.start_with?("..")
|
|
371
|
+
File.join(dir_part, chosen)
|
|
372
|
+
elsif raw_prefix.start_with?("/")
|
|
373
|
+
full
|
|
374
|
+
elsif raw_prefix.end_with?("/")
|
|
375
|
+
"#{raw_prefix}#{chosen}"
|
|
376
|
+
elsif raw_prefix.include?("/")
|
|
377
|
+
File.join(File.dirname(raw_prefix), chosen)
|
|
378
|
+
else
|
|
379
|
+
chosen
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
rendered += "/" if File.directory?(full) && !rendered.end_with?("/")
|
|
383
|
+
escape_path(rendered)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def escape_path(path)
|
|
388
|
+
path.gsub(/([ \t\n\\'"`$!#&()*;<>?\[\]\{\}|])/) { "\\#{$1}" }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def unescape_path(path)
|
|
392
|
+
path.to_s.gsub(/\\(.)/, '\1')
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def expand_path_prefix(prefix)
|
|
396
|
+
s = prefix.to_s
|
|
397
|
+
return if s.empty?
|
|
398
|
+
|
|
399
|
+
if s.start_with?("~/")
|
|
400
|
+
File.join(Dir.home, s.delete_prefix("~/"))
|
|
401
|
+
else
|
|
402
|
+
s
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def build_line_with_completion(completion)
|
|
407
|
+
current = buffer.text.to_s
|
|
408
|
+
target = path_completion_range || buffer.completion_replace_range
|
|
409
|
+
start_i = target.begin
|
|
410
|
+
end_i = target.end
|
|
411
|
+
|
|
412
|
+
before = current[0...start_i].to_s
|
|
413
|
+
after = current[end_i..].to_s
|
|
414
|
+
|
|
415
|
+
"#{before}#{completion}#{after}"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def autosuggestion_visible?
|
|
419
|
+
suggestion = autosuggestion.to_s
|
|
420
|
+
text = buffer.text.to_s
|
|
421
|
+
return false if suggestion.empty?
|
|
422
|
+
return false unless suggestion.start_with?(text)
|
|
423
|
+
|
|
424
|
+
suggestion != text
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def autosuggestion_suffix
|
|
428
|
+
return "" unless autosuggestion_visible?
|
|
429
|
+
|
|
430
|
+
autosuggestion.to_s.delete_prefix(buffer.text.to_s)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def accept_autosuggestion!
|
|
434
|
+
return unless autosuggestion_visible?
|
|
435
|
+
|
|
436
|
+
sync_virtual_suffix!
|
|
437
|
+
buffer.accept_virtual_suffix!
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def popup_completion_candidates
|
|
441
|
+
path_prefix = popup_path_completion_prefix
|
|
442
|
+
if path_prefix
|
|
443
|
+
path = rendered_path_candidates(path_prefix)
|
|
444
|
+
return path if path.any?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
return [] unless @completion_proc
|
|
448
|
+
|
|
449
|
+
Array(@completion_proc.call(buffer))
|
|
450
|
+
.compact
|
|
451
|
+
.map(&:to_s)
|
|
452
|
+
.reject(&:empty?)
|
|
453
|
+
.uniq
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def popup_path_completion_prefix
|
|
457
|
+
prefix = path_completion_prefix
|
|
458
|
+
return if prefix.nil? || prefix.empty?
|
|
459
|
+
|
|
460
|
+
raw_prefix = unescape_path(prefix)
|
|
461
|
+
expanded = expand_path_prefix(raw_prefix)
|
|
462
|
+
return prefix unless File.directory?(expanded)
|
|
463
|
+
|
|
464
|
+
if raw_prefix.end_with?("/")
|
|
465
|
+
prefix
|
|
466
|
+
else
|
|
467
|
+
escape_path("#{raw_prefix}/")
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def popup_completion_query
|
|
472
|
+
popup_path_completion_prefix || path_completion_prefix || buffer.completion_prefix
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def popup_completion_range
|
|
476
|
+
path_completion_range || buffer.completion_replace_range
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def popup_completion_query
|
|
480
|
+
path_completion_prefix || buffer.completion_prefix
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def sync_virtual_suffix!
|
|
484
|
+
buffer.virtual_suffix = autosuggestion_suffix
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private
|
|
488
|
+
|
|
489
|
+
def reset_history_cursor_for(action)
|
|
490
|
+
return if action.to_s.start_with?("history_")
|
|
491
|
+
|
|
492
|
+
history&.reset_cursor_for(resolve_history_kind, ctx: resolve_history_ctx)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def resolve_history_kind
|
|
496
|
+
value = @history_kind
|
|
497
|
+
value = instance_exec(&value) if value.respond_to?(:call)
|
|
498
|
+
value.to_sym
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def resolve_history_ctx
|
|
502
|
+
value = @history_ctx
|
|
503
|
+
value = instance_exec(&value) if value.respond_to?(:call)
|
|
504
|
+
value.is_a?(Hash) ? value : {}
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|