textbringer 18 → 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 +23 -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/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 +1 -0
- 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 +5 -2
- data/lib/textbringer/utils.rb +14 -10
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +36 -9
- 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,568 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "json"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Textbringer
|
|
6
|
+
module LSP
|
|
7
|
+
class Client
|
|
8
|
+
class ServerError < StandardError; end
|
|
9
|
+
class TimeoutError < StandardError; end
|
|
10
|
+
|
|
11
|
+
attr_reader :root_path, :server_name, :server_capabilities
|
|
12
|
+
|
|
13
|
+
def initialize(command:, args: [], root_path:, server_name: nil, workspace_folders: nil)
|
|
14
|
+
@command = command
|
|
15
|
+
@args = args
|
|
16
|
+
@root_path = root_path
|
|
17
|
+
@server_name = server_name || command
|
|
18
|
+
@workspace_folders = Array(workspace_folders || @root_path)
|
|
19
|
+
@stdin = nil
|
|
20
|
+
@stdout = nil
|
|
21
|
+
@stderr = nil
|
|
22
|
+
@wait_thr = nil
|
|
23
|
+
@request_id = 0
|
|
24
|
+
@pending_requests = {}
|
|
25
|
+
@running = false
|
|
26
|
+
@initialized = false
|
|
27
|
+
@reader_thread = nil
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@open_documents = {}
|
|
30
|
+
@server_capabilities = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start
|
|
34
|
+
return if @running
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
# Use Bundler's clean environment if available
|
|
38
|
+
if defined?(Bundler)
|
|
39
|
+
Bundler.with_unbundled_env do
|
|
40
|
+
@stdin, @stdout, @stderr, @wait_thr =
|
|
41
|
+
Open3.popen3(@command, *@args)
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
@stdin, @stdout, @stderr, @wait_thr =
|
|
45
|
+
Open3.popen3(@command, *@args)
|
|
46
|
+
end
|
|
47
|
+
rescue Errno::ENOENT
|
|
48
|
+
Utils.message("LSP server command not found: #{@command}")
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@running = true
|
|
53
|
+
initialize_server_sync
|
|
54
|
+
start_reader_thread
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stop
|
|
58
|
+
return unless @running
|
|
59
|
+
|
|
60
|
+
shutdown
|
|
61
|
+
exit_server
|
|
62
|
+
cleanup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def running?
|
|
66
|
+
@running
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialized?
|
|
70
|
+
@initialized
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Document synchronization
|
|
74
|
+
|
|
75
|
+
def did_open(uri:, language_id:, version:, text:)
|
|
76
|
+
return unless @initialized
|
|
77
|
+
|
|
78
|
+
send_notification("textDocument/didOpen", {
|
|
79
|
+
textDocument: {
|
|
80
|
+
uri: uri,
|
|
81
|
+
languageId: language_id,
|
|
82
|
+
version: version,
|
|
83
|
+
text: text
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
@open_documents[uri] = version
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def did_change(uri:, version:, text: nil, range: nil, range_length: nil)
|
|
90
|
+
return unless @initialized
|
|
91
|
+
return unless @open_documents.key?(uri)
|
|
92
|
+
|
|
93
|
+
# Support both full and incremental sync
|
|
94
|
+
content_change = if range
|
|
95
|
+
# Incremental change
|
|
96
|
+
change = { range: range }
|
|
97
|
+
change[:rangeLength] = range_length if range_length
|
|
98
|
+
change[:text] = text if text
|
|
99
|
+
change
|
|
100
|
+
else
|
|
101
|
+
# Full document sync
|
|
102
|
+
{ text: text }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
send_notification("textDocument/didChange", {
|
|
106
|
+
textDocument: {
|
|
107
|
+
uri: uri,
|
|
108
|
+
version: version
|
|
109
|
+
},
|
|
110
|
+
contentChanges: [content_change]
|
|
111
|
+
})
|
|
112
|
+
@open_documents[uri] = version
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def did_close(uri:)
|
|
116
|
+
return unless @initialized
|
|
117
|
+
return unless @open_documents.key?(uri)
|
|
118
|
+
|
|
119
|
+
send_notification("textDocument/didClose", {
|
|
120
|
+
textDocument: { uri: uri }
|
|
121
|
+
})
|
|
122
|
+
@open_documents.delete(uri)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def document_open?(uri)
|
|
126
|
+
@open_documents.key?(uri)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Completion
|
|
130
|
+
|
|
131
|
+
def completion(uri:, line:, character:, context: nil, &callback)
|
|
132
|
+
return unless @initialized
|
|
133
|
+
|
|
134
|
+
params = {
|
|
135
|
+
textDocument: { uri: uri },
|
|
136
|
+
position: { line: line, character: character }
|
|
137
|
+
}
|
|
138
|
+
params[:context] = context if context
|
|
139
|
+
|
|
140
|
+
send_request("textDocument/completion", params) do |result, error|
|
|
141
|
+
if error
|
|
142
|
+
callback.call(nil, error) if callback
|
|
143
|
+
else
|
|
144
|
+
items = normalize_completion_result(result)
|
|
145
|
+
callback.call(items, nil) if callback
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Signature Help
|
|
151
|
+
|
|
152
|
+
def signature_help(uri:, line:, character:, context: nil, &callback)
|
|
153
|
+
return unless @initialized
|
|
154
|
+
|
|
155
|
+
params = {
|
|
156
|
+
textDocument: { uri: uri },
|
|
157
|
+
position: { line: line, character: character }
|
|
158
|
+
}
|
|
159
|
+
params[:context] = context if context
|
|
160
|
+
|
|
161
|
+
send_request("textDocument/signatureHelp", params) do |result, error|
|
|
162
|
+
callback.call(result, error) if callback
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def initialize_server_sync
|
|
169
|
+
@request_id += 1
|
|
170
|
+
id = @request_id
|
|
171
|
+
|
|
172
|
+
message = {
|
|
173
|
+
jsonrpc: "2.0",
|
|
174
|
+
id: id,
|
|
175
|
+
method: "initialize",
|
|
176
|
+
params: {
|
|
177
|
+
processId: Process.pid,
|
|
178
|
+
rootUri: "file://#{@root_path}",
|
|
179
|
+
rootPath: @root_path,
|
|
180
|
+
workspaceFolders: @workspace_folders.map { |path|
|
|
181
|
+
{ uri: "file://#{path}", name: File.basename(path) }
|
|
182
|
+
},
|
|
183
|
+
capabilities: client_capabilities,
|
|
184
|
+
trace: "off"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
write_message(message)
|
|
189
|
+
|
|
190
|
+
# Check if process is running
|
|
191
|
+
unless @wait_thr&.alive?
|
|
192
|
+
stderr_output = @stderr&.read rescue ""
|
|
193
|
+
Utils.message("LSP server failed to start: #{stderr_output.strip}")
|
|
194
|
+
cleanup
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Read messages synchronously until we get the initialize response.
|
|
199
|
+
# Server requests (e.g. window/workDoneProgress/create) that arrive
|
|
200
|
+
# before the response are handled inline.
|
|
201
|
+
timeout = Time.now + 5 # 5 second timeout
|
|
202
|
+
loop do
|
|
203
|
+
if Time.now > timeout
|
|
204
|
+
stderr_output = @stderr&.read_nonblock(1000, exception: false) rescue ""
|
|
205
|
+
Utils.message("LSP initialization timeout. stderr: #{stderr_output}")
|
|
206
|
+
cleanup
|
|
207
|
+
return
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check if data is available to read (non-blocking check)
|
|
211
|
+
readable, = IO.select([@stdout], nil, nil, 0.1)
|
|
212
|
+
next unless readable
|
|
213
|
+
|
|
214
|
+
msg = read_message
|
|
215
|
+
unless msg
|
|
216
|
+
# Check if process died
|
|
217
|
+
unless @wait_thr&.alive?
|
|
218
|
+
stderr_output = @stderr&.read rescue ""
|
|
219
|
+
Utils.message("LSP server died: #{stderr_output.strip}")
|
|
220
|
+
cleanup
|
|
221
|
+
return
|
|
222
|
+
end
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if msg.key?("id") && msg["id"] == id
|
|
227
|
+
# This is the initialize response
|
|
228
|
+
if msg.key?("error")
|
|
229
|
+
Utils.message(
|
|
230
|
+
"LSP initialization failed: #{msg["error"]["message"]}"
|
|
231
|
+
)
|
|
232
|
+
cleanup
|
|
233
|
+
else
|
|
234
|
+
@server_capabilities = msg["result"]["capabilities"] || {}
|
|
235
|
+
@initialized = true
|
|
236
|
+
send_notification("initialized", {})
|
|
237
|
+
Utils.message("LSP server #{@server_name} initialized")
|
|
238
|
+
end
|
|
239
|
+
return
|
|
240
|
+
elsif msg.key?("id") && msg.key?("method")
|
|
241
|
+
# Server request during initialization - handle it
|
|
242
|
+
handle_server_request(msg)
|
|
243
|
+
end
|
|
244
|
+
# Skip notifications during initialization
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def client_capabilities
|
|
249
|
+
{
|
|
250
|
+
textDocument: {
|
|
251
|
+
completion: {
|
|
252
|
+
completionItem: {
|
|
253
|
+
snippetSupport: false,
|
|
254
|
+
deprecatedSupport: true,
|
|
255
|
+
labelDetailsSupport: true
|
|
256
|
+
},
|
|
257
|
+
completionItemKind: {
|
|
258
|
+
valueSet: (1..25).to_a
|
|
259
|
+
},
|
|
260
|
+
contextSupport: true
|
|
261
|
+
},
|
|
262
|
+
signatureHelp: {
|
|
263
|
+
signatureInformation: {
|
|
264
|
+
documentationFormat: ["plaintext"],
|
|
265
|
+
parameterInformation: {
|
|
266
|
+
labelOffsetSupport: true
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
contextSupport: true
|
|
270
|
+
},
|
|
271
|
+
synchronization: {
|
|
272
|
+
didSave: true,
|
|
273
|
+
willSave: false,
|
|
274
|
+
willSaveWaitUntil: false,
|
|
275
|
+
dynamicRegistration: false
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
workspace: {
|
|
279
|
+
workspaceFolders: true
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def shutdown
|
|
285
|
+
return unless @initialized
|
|
286
|
+
|
|
287
|
+
shutdown_cv = ConditionVariable.new
|
|
288
|
+
send_request("shutdown", nil) do |_result, _error|
|
|
289
|
+
@mutex.synchronize do
|
|
290
|
+
@initialized = false
|
|
291
|
+
shutdown_cv.signal
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Wait for shutdown response with timeout
|
|
296
|
+
@mutex.synchronize do
|
|
297
|
+
shutdown_cv.wait(@mutex, 3) if @initialized
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def exit_server
|
|
302
|
+
send_notification("exit", nil)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def cleanup
|
|
306
|
+
@mutex.synchronize do
|
|
307
|
+
@running = false
|
|
308
|
+
@initialized = false
|
|
309
|
+
@pending_requests.clear
|
|
310
|
+
@open_documents.clear
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Close IO streams first so reader thread exits naturally
|
|
314
|
+
@stdin&.close rescue nil
|
|
315
|
+
@stdout&.close rescue nil
|
|
316
|
+
@stderr&.close rescue nil
|
|
317
|
+
@stdin = @stdout = @stderr = nil
|
|
318
|
+
|
|
319
|
+
# Wait for reader thread to finish, then force kill if needed
|
|
320
|
+
if @reader_thread && @reader_thread != Thread.current
|
|
321
|
+
@reader_thread.join(1)
|
|
322
|
+
@reader_thread.kill if @reader_thread.alive?
|
|
323
|
+
end
|
|
324
|
+
@reader_thread = nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def send_request(method, params, &callback)
|
|
328
|
+
@mutex.synchronize do
|
|
329
|
+
@request_id += 1
|
|
330
|
+
id = @request_id
|
|
331
|
+
|
|
332
|
+
message = {
|
|
333
|
+
jsonrpc: "2.0",
|
|
334
|
+
id: id,
|
|
335
|
+
method: method,
|
|
336
|
+
params: params
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@pending_requests[id] = callback if callback
|
|
340
|
+
write_message(message)
|
|
341
|
+
id
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def send_notification(method, params)
|
|
346
|
+
@mutex.synchronize do
|
|
347
|
+
message = {
|
|
348
|
+
jsonrpc: "2.0",
|
|
349
|
+
method: method,
|
|
350
|
+
params: params
|
|
351
|
+
}
|
|
352
|
+
write_message(message)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# NOTE: Callers must hold @mutex when calling this method.
|
|
357
|
+
def write_message(message)
|
|
358
|
+
return unless @stdin && !@stdin.closed?
|
|
359
|
+
|
|
360
|
+
json = message.to_json
|
|
361
|
+
header = "Content-Length: #{json.bytesize}\r\n\r\n"
|
|
362
|
+
|
|
363
|
+
@stdin.write(header)
|
|
364
|
+
@stdin.write(json)
|
|
365
|
+
@stdin.flush
|
|
366
|
+
rescue IOError, Errno::EPIPE => e
|
|
367
|
+
Utils.message("LSP write error: #{e.message}")
|
|
368
|
+
@running = false
|
|
369
|
+
@initialized = false
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def start_reader_thread
|
|
373
|
+
@reader_thread = Thread.new do
|
|
374
|
+
read_messages
|
|
375
|
+
rescue StandardError => e
|
|
376
|
+
Utils.foreground do
|
|
377
|
+
Utils.message("LSP reader error: #{e.message}")
|
|
378
|
+
end
|
|
379
|
+
cleanup
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def read_messages
|
|
384
|
+
while @running && @stdout && !@stdout.closed?
|
|
385
|
+
message = read_message
|
|
386
|
+
break unless message
|
|
387
|
+
|
|
388
|
+
Utils.foreground do
|
|
389
|
+
handle_message(message)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def read_message
|
|
395
|
+
# Read headers
|
|
396
|
+
headers = {}
|
|
397
|
+
while (line = @stdout.gets)
|
|
398
|
+
line = line.strip
|
|
399
|
+
break if line.empty?
|
|
400
|
+
|
|
401
|
+
if line =~ /\A([^:]+):\s*(.+)\z/
|
|
402
|
+
headers[$1] = $2
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
return nil if headers.empty?
|
|
407
|
+
|
|
408
|
+
# Read content
|
|
409
|
+
content_length = headers["Content-Length"]&.to_i
|
|
410
|
+
return nil unless content_length && content_length > 0
|
|
411
|
+
|
|
412
|
+
content = @stdout.read(content_length)
|
|
413
|
+
return nil unless content
|
|
414
|
+
|
|
415
|
+
JSON.parse(content)
|
|
416
|
+
rescue JSON::ParserError => e
|
|
417
|
+
Utils.foreground do
|
|
418
|
+
Utils.message("LSP JSON parse error: #{e.message}")
|
|
419
|
+
end
|
|
420
|
+
nil
|
|
421
|
+
rescue IOError
|
|
422
|
+
nil
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def handle_message(message)
|
|
426
|
+
if message.key?("id") && message.key?("method")
|
|
427
|
+
# Request from server
|
|
428
|
+
handle_server_request(message)
|
|
429
|
+
elsif message.key?("id")
|
|
430
|
+
# Response to our request
|
|
431
|
+
handle_response(message)
|
|
432
|
+
else
|
|
433
|
+
# Notification from server
|
|
434
|
+
handle_notification(message)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def handle_response(message)
|
|
439
|
+
id = message["id"]
|
|
440
|
+
callback = @mutex.synchronize { @pending_requests.delete(id) }
|
|
441
|
+
return unless callback
|
|
442
|
+
|
|
443
|
+
if message.key?("error")
|
|
444
|
+
callback.call(nil, message["error"])
|
|
445
|
+
else
|
|
446
|
+
callback.call(message["result"], nil)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def handle_server_request(message)
|
|
451
|
+
# Handle server-initiated requests
|
|
452
|
+
id = message["id"]
|
|
453
|
+
method = message["method"]
|
|
454
|
+
|
|
455
|
+
case method
|
|
456
|
+
when "window/workDoneProgress/create"
|
|
457
|
+
# Accept progress token creation
|
|
458
|
+
send_response(id, nil, nil)
|
|
459
|
+
when "client/registerCapability"
|
|
460
|
+
# Accept capability registration
|
|
461
|
+
send_response(id, nil, nil)
|
|
462
|
+
when "workspace/workspaceFolders"
|
|
463
|
+
folders = @workspace_folders.map { |path|
|
|
464
|
+
{ uri: "file://#{path}", name: File.basename(path) }
|
|
465
|
+
}
|
|
466
|
+
send_response(id, folders, nil)
|
|
467
|
+
else
|
|
468
|
+
# Unknown request - respond with method not found
|
|
469
|
+
send_response(id, nil, {
|
|
470
|
+
code: -32601,
|
|
471
|
+
message: "Method not found: #{method}"
|
|
472
|
+
})
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def handle_notification(message)
|
|
477
|
+
method = message["method"]
|
|
478
|
+
params = message["params"]
|
|
479
|
+
|
|
480
|
+
case method
|
|
481
|
+
when "window/logMessage", "window/showMessage"
|
|
482
|
+
type = params["type"]
|
|
483
|
+
text = params["message"]
|
|
484
|
+
# Log messages (type: 1=Error, 2=Warning, 3=Info, 4=Log)
|
|
485
|
+
if type <= 2
|
|
486
|
+
Utils.message("[LSP] #{text}")
|
|
487
|
+
end
|
|
488
|
+
when "textDocument/publishDiagnostics"
|
|
489
|
+
# Could be used for error highlighting in the future
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def send_response(id, result, error)
|
|
494
|
+
@mutex.synchronize do
|
|
495
|
+
message = {
|
|
496
|
+
jsonrpc: "2.0",
|
|
497
|
+
id: id
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if error
|
|
501
|
+
message[:error] = error
|
|
502
|
+
else
|
|
503
|
+
message[:result] = result
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
write_message(message)
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def normalize_completion_result(result)
|
|
511
|
+
return [] if result.nil?
|
|
512
|
+
|
|
513
|
+
items = if result.is_a?(Array)
|
|
514
|
+
result
|
|
515
|
+
elsif result.is_a?(Hash) && result["items"]
|
|
516
|
+
result["items"]
|
|
517
|
+
else
|
|
518
|
+
[]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
items.map { |item| normalize_completion_item(item) }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def normalize_completion_item(item)
|
|
525
|
+
{
|
|
526
|
+
label: item["label"],
|
|
527
|
+
insert_text: item["insertText"] || item["textEdit"]&.dig("newText") || item["label"],
|
|
528
|
+
detail: item["detail"],
|
|
529
|
+
kind: completion_item_kind_name(item["kind"]),
|
|
530
|
+
sort_text: item["sortText"] || item["label"],
|
|
531
|
+
filter_text: item["filterText"] || item["label"]
|
|
532
|
+
}
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
COMPLETION_ITEM_KINDS = {
|
|
536
|
+
1 => "Text",
|
|
537
|
+
2 => "Method",
|
|
538
|
+
3 => "Function",
|
|
539
|
+
4 => "Constructor",
|
|
540
|
+
5 => "Field",
|
|
541
|
+
6 => "Variable",
|
|
542
|
+
7 => "Class",
|
|
543
|
+
8 => "Interface",
|
|
544
|
+
9 => "Module",
|
|
545
|
+
10 => "Property",
|
|
546
|
+
11 => "Unit",
|
|
547
|
+
12 => "Value",
|
|
548
|
+
13 => "Enum",
|
|
549
|
+
14 => "Keyword",
|
|
550
|
+
15 => "Snippet",
|
|
551
|
+
16 => "Color",
|
|
552
|
+
17 => "File",
|
|
553
|
+
18 => "Reference",
|
|
554
|
+
19 => "Folder",
|
|
555
|
+
20 => "EnumMember",
|
|
556
|
+
21 => "Constant",
|
|
557
|
+
22 => "Struct",
|
|
558
|
+
23 => "Event",
|
|
559
|
+
24 => "Operator",
|
|
560
|
+
25 => "TypeParameter"
|
|
561
|
+
}.freeze
|
|
562
|
+
|
|
563
|
+
def completion_item_kind_name(kind)
|
|
564
|
+
COMPLETION_ITEM_KINDS[kind]
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
module LSP
|
|
3
|
+
class ServerRegistry
|
|
4
|
+
# Project root marker files/directories
|
|
5
|
+
PROJECT_ROOT_MARKERS = %w[
|
|
6
|
+
.git
|
|
7
|
+
.hg
|
|
8
|
+
.svn
|
|
9
|
+
Gemfile
|
|
10
|
+
package.json
|
|
11
|
+
Cargo.toml
|
|
12
|
+
go.mod
|
|
13
|
+
setup.py
|
|
14
|
+
pyproject.toml
|
|
15
|
+
Makefile
|
|
16
|
+
CMakeLists.txt
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
@server_configs = []
|
|
20
|
+
@clients = {}
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_reader :server_configs
|
|
24
|
+
|
|
25
|
+
def register(language_id, command:, args: [], file_patterns: [], interpreter_patterns: [], mode: nil)
|
|
26
|
+
config = ServerConfig.new(
|
|
27
|
+
language_id: language_id,
|
|
28
|
+
command: command,
|
|
29
|
+
args: args,
|
|
30
|
+
file_patterns: file_patterns,
|
|
31
|
+
interpreter_patterns: interpreter_patterns,
|
|
32
|
+
mode: mode
|
|
33
|
+
)
|
|
34
|
+
@server_configs << config
|
|
35
|
+
config
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unregister(language_id)
|
|
39
|
+
@server_configs.reject! { |c| c.language_id == language_id }
|
|
40
|
+
# Stop any running clients for this language
|
|
41
|
+
@clients.delete_if do |key, client|
|
|
42
|
+
if key.start_with?("#{language_id}:")
|
|
43
|
+
client.stop rescue nil
|
|
44
|
+
true
|
|
45
|
+
else
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find_config_for_buffer(buffer)
|
|
52
|
+
mode_name = buffer.mode&.name
|
|
53
|
+
|
|
54
|
+
@server_configs.find do |config|
|
|
55
|
+
if config.mode && mode_name
|
|
56
|
+
config.mode == mode_name
|
|
57
|
+
elsif buffer.file_name && !config.file_patterns.empty?
|
|
58
|
+
config.file_patterns.any? { |pattern| pattern.match?(buffer.file_name) }
|
|
59
|
+
else
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get_client_for_buffer(buffer)
|
|
66
|
+
config = find_config_for_buffer(buffer)
|
|
67
|
+
return nil unless config
|
|
68
|
+
|
|
69
|
+
root_path = find_project_root(buffer.file_name || Dir.pwd)
|
|
70
|
+
client_key = "#{config.language_id}:#{root_path}"
|
|
71
|
+
|
|
72
|
+
@clients[client_key] ||= begin
|
|
73
|
+
client = Client.new(
|
|
74
|
+
command: config.command,
|
|
75
|
+
args: config.args,
|
|
76
|
+
root_path: root_path,
|
|
77
|
+
server_name: config.language_id
|
|
78
|
+
)
|
|
79
|
+
client.start
|
|
80
|
+
client
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stop_client_for_buffer(buffer)
|
|
85
|
+
config = find_config_for_buffer(buffer)
|
|
86
|
+
return unless config
|
|
87
|
+
|
|
88
|
+
root_path = find_project_root(buffer.file_name || Dir.pwd)
|
|
89
|
+
client_key = "#{config.language_id}:#{root_path}"
|
|
90
|
+
|
|
91
|
+
client = @clients.delete(client_key)
|
|
92
|
+
client&.stop
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def stop_all_clients
|
|
96
|
+
@clients.each_value do |client|
|
|
97
|
+
client.stop rescue nil
|
|
98
|
+
end
|
|
99
|
+
@clients.clear
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def find_project_root(file_path)
|
|
103
|
+
dir = File.dirname(File.expand_path(file_path))
|
|
104
|
+
|
|
105
|
+
while dir != "/"
|
|
106
|
+
if PROJECT_ROOT_MARKERS.any? { |marker| File.exist?(File.join(dir, marker)) }
|
|
107
|
+
return dir
|
|
108
|
+
end
|
|
109
|
+
parent = File.dirname(dir)
|
|
110
|
+
break if parent == dir
|
|
111
|
+
dir = parent
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Fallback to the file's directory
|
|
115
|
+
File.dirname(File.expand_path(file_path))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def language_id_for_buffer(buffer)
|
|
119
|
+
config = find_config_for_buffer(buffer)
|
|
120
|
+
config&.language_id
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class ServerConfig
|
|
126
|
+
attr_reader :language_id, :command, :args, :file_patterns, :interpreter_patterns, :mode
|
|
127
|
+
|
|
128
|
+
def initialize(language_id:, command:, args:, file_patterns:, interpreter_patterns:, mode:)
|
|
129
|
+
@language_id = language_id
|
|
130
|
+
@command = command
|
|
131
|
+
@args = args
|
|
132
|
+
@file_patterns = file_patterns
|
|
133
|
+
@interpreter_patterns = interpreter_patterns
|
|
134
|
+
@mode = mode
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/textbringer/mode.rb
CHANGED
|
@@ -24,7 +24,9 @@ module Textbringer
|
|
|
24
24
|
|
|
25
25
|
def self.define_generic_command(name, **options)
|
|
26
26
|
command_name = (name.to_s + "_command").intern
|
|
27
|
-
define_command(command_name,
|
|
27
|
+
define_command(command_name,
|
|
28
|
+
source_location_proc: -> { Buffer.current.mode.method(name).source_location rescue nil },
|
|
29
|
+
**options) do |*args|
|
|
28
30
|
begin
|
|
29
31
|
Buffer.current.mode.send(name, *args)
|
|
30
32
|
rescue NoMethodError => e
|