textbringer 17 → 19
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/exe/txtb +1 -1
- data/lib/textbringer/buffer.rb +37 -3
- data/lib/textbringer/commands/buffers.rb +4 -2
- data/lib/textbringer/commands/clipboard.rb +21 -6
- data/lib/textbringer/commands/completion.rb +133 -0
- data/lib/textbringer/commands/ctags.rb +1 -1
- data/lib/textbringer/commands/files.rb +11 -1
- data/lib/textbringer/commands/help.rb +1 -1
- data/lib/textbringer/commands/isearch.rb +4 -10
- data/lib/textbringer/commands/ispell.rb +0 -2
- data/lib/textbringer/commands/lsp.rb +389 -0
- data/lib/textbringer/commands/misc.rb +2 -1
- data/lib/textbringer/commands.rb +7 -3
- data/lib/textbringer/completion_popup.rb +188 -0
- data/lib/textbringer/faces/basic.rb +3 -1
- data/lib/textbringer/faces/completion.rb +4 -0
- data/lib/textbringer/floating_window.rb +327 -0
- data/lib/textbringer/input_methods/skk_input_method.rb +751 -0
- data/lib/textbringer/lsp/client.rb +568 -0
- data/lib/textbringer/lsp/server_registry.rb +138 -0
- data/lib/textbringer/mode.rb +3 -1
- data/lib/textbringer/modes/programming_mode.rb +17 -8
- data/lib/textbringer/modes/transient_mark_mode.rb +9 -2
- data/lib/textbringer/utils.rb +14 -10
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +116 -19
- data/lib/textbringer.rb +7 -0
- data/sig/lib/textbringer/buffer.rbs +483 -0
- data/sig/lib/textbringer/color.rbs +9 -0
- data/sig/lib/textbringer/commands/buffers.rbs +93 -0
- data/sig/lib/textbringer/commands/clipboard.rbs +17 -0
- data/sig/lib/textbringer/commands/completion.rbs +20 -0
- data/sig/lib/textbringer/commands/ctags.rbs +11 -0
- data/sig/lib/textbringer/commands/dabbrev.rbs +4 -0
- data/sig/lib/textbringer/commands/files.rbs +29 -0
- data/sig/lib/textbringer/commands/fill.rbs +5 -0
- data/sig/lib/textbringer/commands/help.rbs +28 -0
- data/sig/lib/textbringer/commands/input_method.rbs +6 -0
- data/sig/lib/textbringer/commands/isearch.rbs +38 -0
- data/sig/lib/textbringer/commands/ispell.rbs +39 -0
- data/sig/lib/textbringer/commands/keyboard_macro.rbs +25 -0
- data/sig/lib/textbringer/commands/lsp.rbs +8 -0
- data/sig/lib/textbringer/commands/misc.rbs +74 -0
- data/sig/lib/textbringer/commands/rectangle.rbs +19 -0
- data/sig/lib/textbringer/commands/register.rbs +31 -0
- data/sig/lib/textbringer/commands/replace.rbs +17 -0
- data/sig/lib/textbringer/commands/server.rbs +31 -0
- data/sig/lib/textbringer/commands/ucs_normalize.rbs +9 -0
- data/sig/lib/textbringer/commands/windows.rbs +45 -0
- data/sig/lib/textbringer/commands.rbs +21 -0
- data/sig/lib/textbringer/completion_popup.rbs +40 -0
- data/sig/lib/textbringer/controller.rbs +58 -0
- data/sig/lib/textbringer/default_output.rbs +7 -0
- data/sig/lib/textbringer/errors.rbs +3 -0
- data/sig/lib/textbringer/face.rbs +19 -0
- data/sig/lib/textbringer/floating_window.rbs +42 -0
- data/sig/lib/textbringer/global_minor_mode.rbs +7 -0
- data/sig/lib/textbringer/input_method.rbs +28 -0
- data/sig/lib/textbringer/input_methods/hangul_input_method.rbs +12 -0
- data/sig/lib/textbringer/input_methods/hiragana_input_method.rbs +12 -0
- data/sig/lib/textbringer/input_methods/t_code_input_method.rbs +49 -0
- data/sig/lib/textbringer/keymap.rbs +33 -0
- data/sig/lib/textbringer/lsp/client.rbs +21 -0
- data/sig/lib/textbringer/lsp/server_registry.rbs +23 -0
- data/sig/lib/textbringer/minor_mode.rbs +12 -0
- data/sig/lib/textbringer/mode.rbs +70 -0
- data/sig/lib/textbringer/modes/backtrace_mode.rbs +8 -0
- data/sig/lib/textbringer/modes/buffer_list_mode.rbs +5 -0
- data/sig/lib/textbringer/modes/c_mode.rbs +21 -0
- data/sig/lib/textbringer/modes/completion_list_mode.rbs +5 -0
- data/sig/lib/textbringer/modes/fundamental_mode.rbs +3 -0
- data/sig/lib/textbringer/modes/help_mode.rbs +7 -0
- data/sig/lib/textbringer/modes/overwrite_mode.rbs +15 -0
- data/sig/lib/textbringer/modes/programming_mode.rbs +14 -0
- data/sig/lib/textbringer/modes/ruby_mode.rbs +57 -0
- data/sig/lib/textbringer/plugin.rbs +3 -0
- data/sig/lib/textbringer/ring.rbs +36 -0
- data/sig/lib/textbringer/utils.rbs +95 -0
- data/sig/lib/textbringer/window.rbs +183 -0
- data/textbringer.gemspec +1 -0
- metadata +76 -2
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
module Commands
|
|
3
|
+
LSP_DOCUMENT_VERSIONS = {}
|
|
4
|
+
LSP_STATUS = {
|
|
5
|
+
signature_window: nil
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
define_command(:lsp_completion, doc: <<~DOC) do
|
|
9
|
+
Request completion from the Language Server Protocol server.
|
|
10
|
+
Uses M-Tab (Alt+Tab) as the default keybinding.
|
|
11
|
+
DOC
|
|
12
|
+
buffer = Buffer.current
|
|
13
|
+
|
|
14
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
15
|
+
unless client
|
|
16
|
+
raise EditorError, "No LSP server configured for this buffer"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless client.running? && client.initialized?
|
|
20
|
+
message("LSP server not ready, please try again")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless client.document_open?(buffer_uri(buffer))
|
|
25
|
+
lsp_open_document(buffer)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get completion position
|
|
29
|
+
uri = buffer_uri(buffer)
|
|
30
|
+
pos = lsp_position(buffer, buffer.point)
|
|
31
|
+
line = pos[:line]
|
|
32
|
+
character = pos[:character]
|
|
33
|
+
|
|
34
|
+
# Calculate the start point for completion (beginning of current word)
|
|
35
|
+
start_point = buffer.save_point do
|
|
36
|
+
buffer.skip_re_backward(buffer.mode.symbol_pattern)
|
|
37
|
+
buffer.point
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get prefix already typed
|
|
41
|
+
prefix = buffer.substring(start_point, buffer.point)
|
|
42
|
+
|
|
43
|
+
# Determine trigger context: if the character immediately before start_point
|
|
44
|
+
# is a completion trigger character, inform the server so it returns
|
|
45
|
+
# member completions (e.g. after ".").
|
|
46
|
+
trigger_chars = client.server_capabilities
|
|
47
|
+
.dig("completionProvider", "triggerCharacters") || []
|
|
48
|
+
char_before_start = buffer.save_point {
|
|
49
|
+
buffer.goto_char(start_point)
|
|
50
|
+
buffer.point > 0 ? (buffer.backward_char; buffer.char_after) : nil
|
|
51
|
+
}
|
|
52
|
+
context = lsp_completion_context(prefix, trigger_chars, char_before_start)
|
|
53
|
+
|
|
54
|
+
# Request completion
|
|
55
|
+
client.completion(uri: uri, line: line, character: character, context: context) do |items, error|
|
|
56
|
+
if error
|
|
57
|
+
message("LSP completion error: #{error["message"]}")
|
|
58
|
+
elsif items && !items.empty?
|
|
59
|
+
# Sort by sort_text (LSP server already filtered based on position)
|
|
60
|
+
sorted_items = items.sort_by { |item| item[:sort_text] || item[:label] }
|
|
61
|
+
|
|
62
|
+
completion_popup_start(
|
|
63
|
+
items: sorted_items,
|
|
64
|
+
start_point: start_point,
|
|
65
|
+
prefix: prefix
|
|
66
|
+
)
|
|
67
|
+
else
|
|
68
|
+
message("No completions found")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
define_command(:lsp_signature_help, doc: <<~DOC) do
|
|
74
|
+
Request signature help from the Language Server Protocol server.
|
|
75
|
+
Displays the signature of the function/method at the current cursor
|
|
76
|
+
position in a floating window.
|
|
77
|
+
DOC
|
|
78
|
+
buffer = Buffer.current
|
|
79
|
+
|
|
80
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
81
|
+
unless client
|
|
82
|
+
raise EditorError, "No LSP server configured for this buffer"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
unless client.running? && client.initialized?
|
|
86
|
+
message("LSP server not ready, please try again")
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
unless client.server_capabilities["signatureHelpProvider"]
|
|
91
|
+
message("LSP server does not support signature help")
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
unless client.document_open?(buffer_uri(buffer))
|
|
96
|
+
lsp_open_document(buffer)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
uri = buffer_uri(buffer)
|
|
100
|
+
pos = lsp_position(buffer, buffer.point)
|
|
101
|
+
|
|
102
|
+
# Determine trigger character from the character before point
|
|
103
|
+
trigger_char = nil
|
|
104
|
+
trigger_chars = client.server_capabilities
|
|
105
|
+
.dig("signatureHelpProvider", "triggerCharacters") || []
|
|
106
|
+
if buffer.point > 0
|
|
107
|
+
char_before = buffer.save_point {
|
|
108
|
+
buffer.backward_char
|
|
109
|
+
buffer.char_after
|
|
110
|
+
}
|
|
111
|
+
trigger_char = char_before if trigger_chars.include?(char_before)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
context = if trigger_char
|
|
115
|
+
{
|
|
116
|
+
triggerKind: 2, # TriggerCharacter
|
|
117
|
+
triggerCharacter: trigger_char,
|
|
118
|
+
isRetrigger: false
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
{
|
|
122
|
+
triggerKind: 1, # Invoked
|
|
123
|
+
isRetrigger: false
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
client.signature_help(uri: uri, line: pos[:line], character: pos[:character], context: context) do |result, error|
|
|
128
|
+
if error
|
|
129
|
+
message("LSP signature help error: #{error["message"]}")
|
|
130
|
+
elsif result && result["signatures"] && !result["signatures"].empty?
|
|
131
|
+
active_index = result["activeSignature"] || 0
|
|
132
|
+
signature = result["signatures"][active_index]
|
|
133
|
+
label = signature["label"] if signature
|
|
134
|
+
if label
|
|
135
|
+
lsp_show_signature_window(label)
|
|
136
|
+
else
|
|
137
|
+
message("No signature information available")
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
message("No signature information available")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
define_command(:lsp_ensure_started, doc: <<~DOC) do
|
|
146
|
+
Start the LSP server for the current buffer if not already running.
|
|
147
|
+
DOC
|
|
148
|
+
buffer = Buffer.current
|
|
149
|
+
|
|
150
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
151
|
+
if client
|
|
152
|
+
if client.running?
|
|
153
|
+
message("LSP server already running")
|
|
154
|
+
else
|
|
155
|
+
message("Starting LSP server...")
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
message("No LSP server configured for this buffer")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
define_command(:lsp_stop, doc: <<~DOC) do
|
|
163
|
+
Stop the LSP server for the current buffer.
|
|
164
|
+
DOC
|
|
165
|
+
buffer = Buffer.current
|
|
166
|
+
LSP::ServerRegistry.stop_client_for_buffer(buffer)
|
|
167
|
+
message("LSP server stopped")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
define_command(:lsp_restart, doc: <<~DOC) do
|
|
171
|
+
Restart the LSP server for the current buffer.
|
|
172
|
+
DOC
|
|
173
|
+
buffer = Buffer.current
|
|
174
|
+
LSP::ServerRegistry.stop_client_for_buffer(buffer)
|
|
175
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
176
|
+
if client
|
|
177
|
+
message("LSP server restarting...")
|
|
178
|
+
else
|
|
179
|
+
message("No LSP server configured for this file type")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Helper methods
|
|
184
|
+
|
|
185
|
+
def lsp_completion_context(prefix, trigger_chars, char_before_start)
|
|
186
|
+
if prefix.empty? && trigger_chars.include?(char_before_start)
|
|
187
|
+
{ triggerKind: 2, # TriggerCharacter
|
|
188
|
+
triggerCharacter: char_before_start }
|
|
189
|
+
else
|
|
190
|
+
{ triggerKind: 1 } # Invoked
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Resolve textDocumentSync to a TextDocumentSyncKind integer.
|
|
195
|
+
# The server capability may be an Integer or a TextDocumentSyncOptions Hash.
|
|
196
|
+
# Returns 0 (None), 1 (Full), or 2 (Incremental). Defaults to 2.
|
|
197
|
+
def lsp_text_document_sync_kind(server_capabilities)
|
|
198
|
+
sync = server_capabilities["textDocumentSync"]
|
|
199
|
+
case sync
|
|
200
|
+
when Integer then sync
|
|
201
|
+
when Hash then sync["change"]&.to_i || 2
|
|
202
|
+
else 2
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Convert a string's character length to UTF-16 code unit count.
|
|
207
|
+
# LSP positions use UTF-16 offsets by default.
|
|
208
|
+
def lsp_utf16_length(str)
|
|
209
|
+
str.encode("UTF-16LE").bytesize / 2
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Compute LSP position (0-based line, UTF-16 character offset)
|
|
213
|
+
# from a buffer position.
|
|
214
|
+
def lsp_position(buffer, pos)
|
|
215
|
+
line, = buffer.pos_to_line_and_column(pos)
|
|
216
|
+
# Get the text from the start of the line to compute UTF-16 offset
|
|
217
|
+
line_start = buffer.save_point do
|
|
218
|
+
buffer.goto_char(pos)
|
|
219
|
+
buffer.beginning_of_line
|
|
220
|
+
buffer.point
|
|
221
|
+
end
|
|
222
|
+
text_on_line = buffer.substring(line_start, pos)
|
|
223
|
+
character = lsp_utf16_length(text_on_line)
|
|
224
|
+
{ line: line - 1, character: character }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def buffer_uri(buffer)
|
|
228
|
+
if buffer.file_name
|
|
229
|
+
"file://#{buffer.file_name}"
|
|
230
|
+
else
|
|
231
|
+
"untitled:#{buffer.name}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def lsp_open_document(buffer)
|
|
236
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
237
|
+
return unless client
|
|
238
|
+
return unless client.running? && client.initialized?
|
|
239
|
+
|
|
240
|
+
uri = buffer_uri(buffer)
|
|
241
|
+
return if client.document_open?(uri)
|
|
242
|
+
|
|
243
|
+
version = 1
|
|
244
|
+
LSP_DOCUMENT_VERSIONS[uri] = version
|
|
245
|
+
language_id = LSP::ServerRegistry.language_id_for_buffer(buffer) || "text"
|
|
246
|
+
client.did_open(
|
|
247
|
+
uri: uri,
|
|
248
|
+
language_id: language_id,
|
|
249
|
+
version: version,
|
|
250
|
+
text: buffer.to_s
|
|
251
|
+
)
|
|
252
|
+
unless buffer[:lsp_hooks_installed]
|
|
253
|
+
lsp_setup_buffer_hooks(buffer, client, uri)
|
|
254
|
+
buffer[:lsp_hooks_installed] = true
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Set up buffer hooks for document synchronization
|
|
259
|
+
def lsp_setup_buffer_hooks(buffer, client, uri)
|
|
260
|
+
# Track changes and send updates to LSP server.
|
|
261
|
+
# Sync kind is determined by the server's textDocumentSync capability:
|
|
262
|
+
# 1 = Full (send complete document text on every change)
|
|
263
|
+
# 2 = Incremental (send only the changed range)
|
|
264
|
+
add_hook(:after_change_functions, local: true) do |beg_pos, end_pos, old_text|
|
|
265
|
+
next unless client.running? && client.document_open?(uri)
|
|
266
|
+
|
|
267
|
+
version = LSP_DOCUMENT_VERSIONS[uri] || 0
|
|
268
|
+
version += 1
|
|
269
|
+
LSP_DOCUMENT_VERSIONS[uri] = version
|
|
270
|
+
|
|
271
|
+
sync_kind = lsp_text_document_sync_kind(client.server_capabilities)
|
|
272
|
+
|
|
273
|
+
next if sync_kind == 0 # None: server does not want change notifications
|
|
274
|
+
|
|
275
|
+
if sync_kind == 1
|
|
276
|
+
# Full document sync (e.g. solargraph). Note: buffer.to_s is called
|
|
277
|
+
# on every change; this is expected behavior for Full-sync servers.
|
|
278
|
+
client.did_change(uri: uri, version: version, text: buffer.to_s)
|
|
279
|
+
else
|
|
280
|
+
# Incremental sync
|
|
281
|
+
# Compute start position in LSP coordinates (0-based, UTF-16)
|
|
282
|
+
start_pos = lsp_position(buffer, beg_pos)
|
|
283
|
+
|
|
284
|
+
if old_text.empty?
|
|
285
|
+
# Insertion: old range is empty, new text is the inserted content
|
|
286
|
+
new_text = buffer.substring(beg_pos, end_pos)
|
|
287
|
+
range = { start: start_pos, end: start_pos }
|
|
288
|
+
else
|
|
289
|
+
# Deletion: compute old end position from the deleted text
|
|
290
|
+
newline_count = old_text.count("\n")
|
|
291
|
+
if newline_count == 0
|
|
292
|
+
end_line = start_pos[:line]
|
|
293
|
+
end_char = start_pos[:character] + lsp_utf16_length(old_text)
|
|
294
|
+
else
|
|
295
|
+
end_line = start_pos[:line] + newline_count
|
|
296
|
+
last_newline = old_text.rindex("\n")
|
|
297
|
+
end_char = lsp_utf16_length(old_text[last_newline + 1..])
|
|
298
|
+
end
|
|
299
|
+
range = {
|
|
300
|
+
start: start_pos,
|
|
301
|
+
end: { line: end_line, character: end_char }
|
|
302
|
+
}
|
|
303
|
+
new_text = ""
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
client.did_change(
|
|
307
|
+
uri: uri, version: version,
|
|
308
|
+
text: new_text, range: range
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Close document when buffer is killed
|
|
314
|
+
buffer.on_killed do
|
|
315
|
+
if client.running?
|
|
316
|
+
client.did_close(uri: uri)
|
|
317
|
+
LSP_DOCUMENT_VERSIONS.delete(uri)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def lsp_show_signature_window(label)
|
|
323
|
+
# Close any existing signature window
|
|
324
|
+
lsp_close_signature_window
|
|
325
|
+
|
|
326
|
+
columns = [[Buffer.display_width(label) + 2, Curses.cols - 2].min, 1].max
|
|
327
|
+
win = FloatingWindow.at_cursor(
|
|
328
|
+
lines: 1,
|
|
329
|
+
columns: columns
|
|
330
|
+
)
|
|
331
|
+
win.buffer.insert(label)
|
|
332
|
+
win.buffer.beginning_of_buffer
|
|
333
|
+
win.show
|
|
334
|
+
LSP_STATUS[:signature_window] = win
|
|
335
|
+
|
|
336
|
+
add_hook(:pre_command_hook, :lsp_signature_pre_command_hook)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def lsp_close_signature_window
|
|
340
|
+
win = LSP_STATUS[:signature_window]
|
|
341
|
+
if win && !win.deleted?
|
|
342
|
+
win.close
|
|
343
|
+
end
|
|
344
|
+
LSP_STATUS[:signature_window] = nil
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def lsp_signature_pre_command_hook
|
|
348
|
+
lsp_close_signature_window
|
|
349
|
+
remove_hook(:pre_command_hook, :lsp_signature_pre_command_hook)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Keybinding: M-Tab for LSP completion
|
|
353
|
+
GLOBAL_MAP.define_key("\M-\t", :lsp_completion)
|
|
354
|
+
|
|
355
|
+
# Keybinding: F1 s for LSP signature help
|
|
356
|
+
GLOBAL_MAP.define_key([:f1, "s"], :lsp_signature_help)
|
|
357
|
+
|
|
358
|
+
# Open document with LSP server when a file is opened
|
|
359
|
+
HOOKS[:find_file_hook].unshift(:lsp_find_file_hook)
|
|
360
|
+
|
|
361
|
+
def lsp_find_file_hook
|
|
362
|
+
lsp_open_document(Buffer.current)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Reopen document when file name changes
|
|
366
|
+
HOOKS[:after_set_visited_file_name_hook].unshift(
|
|
367
|
+
:lsp_after_set_visited_file_name_hook
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def lsp_after_set_visited_file_name_hook(old_file_name)
|
|
371
|
+
buffer = Buffer.current
|
|
372
|
+
|
|
373
|
+
# Close the old document
|
|
374
|
+
old_uri = old_file_name ? "file://#{old_file_name}" : nil
|
|
375
|
+
return unless old_uri
|
|
376
|
+
client = LSP::ServerRegistry.get_client_for_buffer(buffer)
|
|
377
|
+
if client&.running? && client.document_open?(old_uri)
|
|
378
|
+
client.did_close(uri: old_uri)
|
|
379
|
+
LSP_DOCUMENT_VERSIONS.delete(old_uri)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Reset hooks so they are reinstalled with the new URI
|
|
383
|
+
buffer[:lsp_hooks_installed] = false
|
|
384
|
+
|
|
385
|
+
# Open the new document
|
|
386
|
+
lsp_open_document(buffer)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
@@ -55,6 +55,7 @@ module Textbringer
|
|
|
55
55
|
result = eval(buffer.substring(b, e), TOPLEVEL_BINDING,
|
|
56
56
|
"(eval_region)", 1)
|
|
57
57
|
message(result.inspect)
|
|
58
|
+
buffer.deactivate_mark
|
|
58
59
|
result
|
|
59
60
|
end
|
|
60
61
|
|
|
@@ -303,7 +304,7 @@ module Textbringer
|
|
|
303
304
|
buffer.insert(s)
|
|
304
305
|
Window.redisplay
|
|
305
306
|
rescue EOFError
|
|
306
|
-
throw(:finish)
|
|
307
|
+
throw(:finish) if output.eof? && error.eof?
|
|
307
308
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
308
309
|
Window.redisplay
|
|
309
310
|
next
|
data/lib/textbringer/commands.rb
CHANGED
|
@@ -2,7 +2,11 @@ require "open3"
|
|
|
2
2
|
require "io/wait"
|
|
3
3
|
|
|
4
4
|
module Textbringer
|
|
5
|
-
Command
|
|
5
|
+
class Command < Data.define(:name, :block, :doc, :source_location_proc)
|
|
6
|
+
def source_location
|
|
7
|
+
source_location_proc&.call || block.source_location
|
|
8
|
+
end
|
|
9
|
+
end
|
|
6
10
|
|
|
7
11
|
module Commands
|
|
8
12
|
include Utils
|
|
@@ -21,11 +25,11 @@ module Textbringer
|
|
|
21
25
|
@command_table[name.intern]
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
def define_command(name, doc: "No documentation", &block)
|
|
28
|
+
def define_command(name, doc: "No documentation", source_location_proc: nil, &block)
|
|
25
29
|
name = name.intern
|
|
26
30
|
Commands.send(:define_method, name, &block)
|
|
27
31
|
Commands.send(:module_function, name)
|
|
28
|
-
Commands.command_table[name] = Command.new(name, block, doc)
|
|
32
|
+
Commands.command_table[name] = Command.new(name, block, doc, source_location_proc)
|
|
29
33
|
name
|
|
30
34
|
end
|
|
31
35
|
module_function :define_command
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
class CompletionPopup
|
|
3
|
+
MAX_VISIBLE_ITEMS = 10
|
|
4
|
+
MIN_WIDTH = 20
|
|
5
|
+
MAX_WIDTH = 60
|
|
6
|
+
|
|
7
|
+
attr_reader :items, :selected_index, :start_point
|
|
8
|
+
|
|
9
|
+
def self.instance
|
|
10
|
+
@instance ||= new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@floating_window = nil
|
|
15
|
+
@items = []
|
|
16
|
+
@selected_index = 0
|
|
17
|
+
@start_point = nil
|
|
18
|
+
@prefix = ""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show(items:, start_point:, prefix: "")
|
|
22
|
+
@items = items
|
|
23
|
+
@start_point = start_point
|
|
24
|
+
@prefix = prefix
|
|
25
|
+
@selected_index = 0
|
|
26
|
+
|
|
27
|
+
return if @items.empty?
|
|
28
|
+
|
|
29
|
+
create_or_update_window
|
|
30
|
+
render
|
|
31
|
+
@floating_window.show
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def hide
|
|
35
|
+
@floating_window&.hide
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def close
|
|
39
|
+
if @floating_window
|
|
40
|
+
@floating_window.close
|
|
41
|
+
@floating_window = nil
|
|
42
|
+
end
|
|
43
|
+
@items = []
|
|
44
|
+
@selected_index = 0
|
|
45
|
+
@start_point = nil
|
|
46
|
+
@prefix = ""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visible?
|
|
50
|
+
@floating_window&.visible? || false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def select_next
|
|
54
|
+
return unless visible? && !@items.empty?
|
|
55
|
+
@selected_index = (@selected_index + 1) % @items.size
|
|
56
|
+
render
|
|
57
|
+
@floating_window.redisplay
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def select_previous
|
|
61
|
+
return unless visible? && !@items.empty?
|
|
62
|
+
@selected_index = (@selected_index - 1) % @items.size
|
|
63
|
+
render
|
|
64
|
+
@floating_window.redisplay
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def accept
|
|
68
|
+
return nil unless visible? && !@items.empty?
|
|
69
|
+
item = current_item
|
|
70
|
+
close
|
|
71
|
+
item
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cancel
|
|
75
|
+
close
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def current_item
|
|
80
|
+
return nil if @items.empty?
|
|
81
|
+
@items[@selected_index]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def create_or_update_window
|
|
87
|
+
lines = visible_item_count
|
|
88
|
+
columns = calculate_width
|
|
89
|
+
|
|
90
|
+
if @floating_window && !@floating_window.deleted?
|
|
91
|
+
@floating_window.resize(lines, columns)
|
|
92
|
+
y, x = FloatingWindow.calculate_cursor_position(lines, columns, Window.current)
|
|
93
|
+
@floating_window.move_to(y: y, x: x)
|
|
94
|
+
else
|
|
95
|
+
@floating_window = FloatingWindow.at_cursor(
|
|
96
|
+
lines: lines,
|
|
97
|
+
columns: columns,
|
|
98
|
+
face: :completion_popup,
|
|
99
|
+
current_line_face: :completion_popup_selected
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def visible_item_count
|
|
105
|
+
[@items.size, MAX_VISIBLE_ITEMS].min
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def calculate_width
|
|
109
|
+
max_label_width = @items.map { |item| display_width(item[:label]) }.max || 0
|
|
110
|
+
max_detail_width = @items.map { |item|
|
|
111
|
+
item[:detail] ? display_width(item[:detail]) + 2 : 0
|
|
112
|
+
}.max || 0
|
|
113
|
+
|
|
114
|
+
width = max_label_width + max_detail_width + 2 # padding
|
|
115
|
+
[[width, MIN_WIDTH].max, MAX_WIDTH].min
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def display_width(str)
|
|
119
|
+
return 0 unless str
|
|
120
|
+
Buffer.display_width(str)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def render
|
|
124
|
+
buffer = @floating_window.buffer
|
|
125
|
+
buffer.read_only = false
|
|
126
|
+
begin
|
|
127
|
+
buffer.clear
|
|
128
|
+
|
|
129
|
+
# Calculate visible range with scroll
|
|
130
|
+
visible_count = visible_item_count
|
|
131
|
+
scroll_offset = calculate_scroll_offset(visible_count)
|
|
132
|
+
|
|
133
|
+
visible_items = @items[scroll_offset, visible_count]
|
|
134
|
+
visible_items.each_with_index do |item, index|
|
|
135
|
+
line = format_item(item)
|
|
136
|
+
buffer.insert(line)
|
|
137
|
+
buffer.insert("\n")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Go to the selected line so current_line_face highlights it
|
|
141
|
+
relative_index = @selected_index - scroll_offset
|
|
142
|
+
buffer.goto_line(relative_index + 1)
|
|
143
|
+
buffer.beginning_of_line
|
|
144
|
+
ensure
|
|
145
|
+
buffer.read_only = true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def calculate_scroll_offset(visible_count)
|
|
150
|
+
if @selected_index < visible_count
|
|
151
|
+
0
|
|
152
|
+
else
|
|
153
|
+
@selected_index - visible_count + 1
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def format_item(item)
|
|
158
|
+
label = item[:label] || ""
|
|
159
|
+
detail = item[:detail]
|
|
160
|
+
|
|
161
|
+
# Build the display string
|
|
162
|
+
result = label
|
|
163
|
+
if detail
|
|
164
|
+
result = "#{label} #{detail}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Truncate if too long
|
|
168
|
+
width = calculate_width
|
|
169
|
+
if display_width(result) > width
|
|
170
|
+
result = truncate_to_width(result, width - 1) + "…"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
result
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def truncate_to_width(str, max_width)
|
|
177
|
+
result = +""
|
|
178
|
+
current_width = 0
|
|
179
|
+
str.each_char do |char|
|
|
180
|
+
char_width = Buffer.display_width(char)
|
|
181
|
+
break if current_width + char_width > max_width
|
|
182
|
+
result << char
|
|
183
|
+
current_width += char_width
|
|
184
|
+
end
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -2,5 +2,7 @@ module Textbringer
|
|
|
2
2
|
Face.define :mode_line, reverse: true
|
|
3
3
|
Face.define :link, foreground: "blue", bold: true
|
|
4
4
|
Face.define :control
|
|
5
|
-
Face.define :region, background: "blue"
|
|
5
|
+
Face.define :region, background: "blue", foreground: "white"
|
|
6
|
+
Face.define :isearch, background: "yellow", foreground: "black"
|
|
7
|
+
Face.define :floating_window, background: "cyan", foreground: "black"
|
|
6
8
|
end
|