mui-lsp 0.1.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/.rubocop_todo.yml +73 -0
- data/CHANGELOG.md +61 -0
- data/LICENSE.txt +21 -0
- data/README.md +188 -0
- data/Rakefile +12 -0
- data/lib/mui/lsp/client.rb +290 -0
- data/lib/mui/lsp/handlers/base.rb +92 -0
- data/lib/mui/lsp/handlers/completion.rb +139 -0
- data/lib/mui/lsp/handlers/definition.rb +99 -0
- data/lib/mui/lsp/handlers/diagnostics.rb +161 -0
- data/lib/mui/lsp/handlers/hover.rb +55 -0
- data/lib/mui/lsp/handlers/references.rb +65 -0
- data/lib/mui/lsp/handlers.rb +8 -0
- data/lib/mui/lsp/highlighters/diagnostic_highlighter.rb +71 -0
- data/lib/mui/lsp/json_rpc_io.rb +120 -0
- data/lib/mui/lsp/manager.rb +366 -0
- data/lib/mui/lsp/plugin.rb +539 -0
- data/lib/mui/lsp/protocol/diagnostic.rb +88 -0
- data/lib/mui/lsp/protocol/location.rb +42 -0
- data/lib/mui/lsp/protocol/position.rb +31 -0
- data/lib/mui/lsp/protocol/range.rb +34 -0
- data/lib/mui/lsp/protocol.rb +6 -0
- data/lib/mui/lsp/request_manager.rb +72 -0
- data/lib/mui/lsp/server_config.rb +115 -0
- data/lib/mui/lsp/text_document_sync.rb +149 -0
- data/lib/mui/lsp/version.rb +7 -0
- data/lib/mui/lsp.rb +19 -0
- data/lib/mui_lsp.rb +3 -0
- data/sig/mui/lsp.rbs +6 -0
- metadata +89 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mui"
|
|
4
|
+
|
|
5
|
+
module Mui
|
|
6
|
+
module Lsp
|
|
7
|
+
# Main plugin class for mui-lsp
|
|
8
|
+
# Registers commands and keymaps for LSP integration
|
|
9
|
+
class Plugin < Mui::Plugin
|
|
10
|
+
name "lsp"
|
|
11
|
+
|
|
12
|
+
def setup
|
|
13
|
+
register_commands
|
|
14
|
+
register_keymaps
|
|
15
|
+
register_autocmds
|
|
16
|
+
setup_default_servers
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def register_commands
|
|
22
|
+
# :LspStart - Start an LSP server
|
|
23
|
+
command(:LspStart) do |ctx, args|
|
|
24
|
+
handle_lsp_start(ctx, args)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# :LspStop - Stop an LSP server
|
|
28
|
+
command(:LspStop) do |ctx, args|
|
|
29
|
+
handle_lsp_stop(ctx, args)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# :LspStatus - Show LSP server status
|
|
33
|
+
command(:LspStatus) do |ctx, _args|
|
|
34
|
+
handle_lsp_status(ctx)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# :LspHover - Show hover information
|
|
38
|
+
command(:LspHover) do |ctx, _args|
|
|
39
|
+
handle_lsp_hover(ctx)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# :LspDefinition - Go to definition
|
|
43
|
+
command(:LspDefinition) do |ctx, _args|
|
|
44
|
+
handle_lsp_definition(ctx)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# :LspReferences - Show references
|
|
48
|
+
command(:LspReferences) do |ctx, _args|
|
|
49
|
+
handle_lsp_references(ctx)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# :LspCompletion - Show completion
|
|
53
|
+
command(:LspCompletion) do |ctx, _args|
|
|
54
|
+
handle_lsp_completion(ctx)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# :LspDiagnostics - Show diagnostics
|
|
58
|
+
command(:LspDiagnostics) do |ctx, _args|
|
|
59
|
+
handle_lsp_diagnostics(ctx)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# :LspDiagnosticShow - Show diagnostic at cursor in floating window
|
|
63
|
+
command(:LspDiagnosticShow) do |ctx, _args|
|
|
64
|
+
handle_lsp_diagnostic_show(ctx)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# :LspDebug - Show debug information
|
|
68
|
+
command(:LspDebug) do |ctx, _args|
|
|
69
|
+
handle_lsp_debug(ctx)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# :LspLog - Show LSP log in a buffer
|
|
73
|
+
command(:LspLog) do |ctx, _args|
|
|
74
|
+
handle_lsp_log(ctx)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# :LspOpen - Notify LSP server about current file
|
|
78
|
+
command(:LspOpen) do |ctx, _args|
|
|
79
|
+
handle_lsp_open(ctx)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def register_keymaps
|
|
84
|
+
# K - Show hover
|
|
85
|
+
keymap(:normal, "K") do |ctx|
|
|
86
|
+
handle_lsp_hover(ctx)
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Leader key (\) - start pending mode
|
|
91
|
+
keymap(:normal, "\\") do |_ctx|
|
|
92
|
+
@leader_pending = true
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# \d - Go to definition
|
|
97
|
+
keymap(:normal, "d") do |ctx|
|
|
98
|
+
if @leader_pending
|
|
99
|
+
@leader_pending = false
|
|
100
|
+
handle_lsp_definition(ctx)
|
|
101
|
+
true
|
|
102
|
+
else
|
|
103
|
+
false # Let default 'd' handle it
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# \r - Go to references
|
|
108
|
+
keymap(:normal, "r") do |ctx|
|
|
109
|
+
if @leader_pending
|
|
110
|
+
@leader_pending = false
|
|
111
|
+
handle_lsp_references(ctx)
|
|
112
|
+
true
|
|
113
|
+
else
|
|
114
|
+
false # Let default 'r' handle it
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# \h - Show hover (alternative to K)
|
|
119
|
+
keymap(:normal, "h") do |ctx|
|
|
120
|
+
if @leader_pending
|
|
121
|
+
@leader_pending = false
|
|
122
|
+
handle_lsp_hover(ctx)
|
|
123
|
+
true
|
|
124
|
+
else
|
|
125
|
+
false # Let default 'h' handle it
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# \c - Show completion
|
|
130
|
+
keymap(:normal, "c") do |ctx|
|
|
131
|
+
if @leader_pending
|
|
132
|
+
@leader_pending = false
|
|
133
|
+
handle_lsp_completion(ctx)
|
|
134
|
+
true
|
|
135
|
+
else
|
|
136
|
+
false # Let default 'c' handle it
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# \e - Show diagnostic at cursor
|
|
141
|
+
keymap(:normal, "e") do |ctx|
|
|
142
|
+
if @leader_pending
|
|
143
|
+
@leader_pending = false
|
|
144
|
+
handle_lsp_diagnostic_show(ctx)
|
|
145
|
+
true
|
|
146
|
+
else
|
|
147
|
+
false # Let default 'e' handle it
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Cancel leader pending on any other key (via Escape)
|
|
152
|
+
keymap(:normal, "\e") do |_ctx|
|
|
153
|
+
if @leader_pending
|
|
154
|
+
@leader_pending = false
|
|
155
|
+
true
|
|
156
|
+
else
|
|
157
|
+
false
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def register_autocmds
|
|
163
|
+
# Hook into buffer open/enter events
|
|
164
|
+
autocmd(:BufEnter) do |ctx|
|
|
165
|
+
file_path = ctx.buffer.file_path
|
|
166
|
+
next unless file_path && !file_path.start_with?("[")
|
|
167
|
+
|
|
168
|
+
text = ctx.buffer.lines.join("\n")
|
|
169
|
+
get_manager(ctx.editor).did_open(file_path: file_path, text: text)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Hook into text change events
|
|
173
|
+
autocmd(:TextChanged) do |ctx|
|
|
174
|
+
file_path = ctx.buffer.file_path
|
|
175
|
+
next unless file_path && !file_path.start_with?("[")
|
|
176
|
+
|
|
177
|
+
text = ctx.buffer.lines.join("\n")
|
|
178
|
+
get_manager(ctx.editor).did_change(file_path: file_path, text: text)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Hook into buffer save events
|
|
182
|
+
autocmd(:BufWritePost) do |ctx|
|
|
183
|
+
file_path = ctx.buffer.file_path
|
|
184
|
+
next unless file_path && !file_path.start_with?("[")
|
|
185
|
+
|
|
186
|
+
text = ctx.buffer.lines.join("\n")
|
|
187
|
+
get_manager(ctx.editor).did_save(file_path: file_path, text: text)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Hook into buffer leave/close events
|
|
191
|
+
autocmd(:BufLeave) do |ctx|
|
|
192
|
+
file_path = ctx.buffer.file_path
|
|
193
|
+
next unless file_path && !file_path.start_with?("[")
|
|
194
|
+
|
|
195
|
+
get_manager(ctx.editor).did_close(file_path: file_path)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def setup_default_servers
|
|
200
|
+
# Load server configs from .muirc DSL (Mui.lsp { use :solargraph })
|
|
201
|
+
@default_server_configs = Mui.lsp_server_configs.dup
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Command handlers
|
|
205
|
+
|
|
206
|
+
def handle_lsp_start(ctx, args)
|
|
207
|
+
server_name = args.to_s.strip
|
|
208
|
+
mgr = get_manager(ctx.editor)
|
|
209
|
+
if server_name.empty?
|
|
210
|
+
ctx.set_message("Usage: :LspStart <server_name>")
|
|
211
|
+
ctx.set_message("Available: #{mgr.registered_servers.join(", ")}")
|
|
212
|
+
return
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
mgr.start_server(server_name)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
ctx.set_message("LSP Error: #{e.message}")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def handle_lsp_stop(ctx, args)
|
|
223
|
+
server_name = args.to_s.strip
|
|
224
|
+
mgr = get_manager(ctx.editor)
|
|
225
|
+
if server_name.empty?
|
|
226
|
+
# Stop all servers
|
|
227
|
+
mgr.stop_all
|
|
228
|
+
ctx.set_message("LSP: all servers stopped")
|
|
229
|
+
else
|
|
230
|
+
mgr.stop_server(server_name)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def handle_lsp_status(ctx)
|
|
235
|
+
mgr = get_manager(ctx.editor)
|
|
236
|
+
running = mgr.running_servers
|
|
237
|
+
starting = mgr.starting_servers
|
|
238
|
+
registered = mgr.registered_servers
|
|
239
|
+
|
|
240
|
+
parts = []
|
|
241
|
+
parts << "running: #{running.join(", ")}" unless running.empty?
|
|
242
|
+
parts << "starting: #{starting.join(", ")}" unless starting.empty?
|
|
243
|
+
|
|
244
|
+
if parts.empty?
|
|
245
|
+
ctx.set_message("LSP: no servers running. Registered: #{registered.join(", ")}")
|
|
246
|
+
else
|
|
247
|
+
ctx.set_message("LSP: #{parts.join(", ")} (registered: #{registered.join(", ")})")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def handle_lsp_hover(ctx)
|
|
252
|
+
file_path = ctx.buffer.file_path
|
|
253
|
+
unless file_path
|
|
254
|
+
ctx.set_message("LSP: no file path")
|
|
255
|
+
return
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
line = ctx.window.cursor_row
|
|
259
|
+
character = ctx.window.cursor_col
|
|
260
|
+
get_manager(ctx.editor).hover(file_path: file_path, line: line, character: character)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def handle_lsp_definition(ctx)
|
|
264
|
+
file_path = ctx.buffer.file_path
|
|
265
|
+
unless file_path
|
|
266
|
+
ctx.set_message("LSP: no file path")
|
|
267
|
+
return
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
line = ctx.window.cursor_row
|
|
271
|
+
character = ctx.window.cursor_col
|
|
272
|
+
get_manager(ctx.editor).definition(file_path: file_path, line: line, character: character)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def handle_lsp_references(ctx)
|
|
276
|
+
file_path = ctx.buffer.file_path
|
|
277
|
+
unless file_path
|
|
278
|
+
ctx.set_message("LSP: no file path")
|
|
279
|
+
return
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
line = ctx.window.cursor_row
|
|
283
|
+
character = ctx.window.cursor_col
|
|
284
|
+
get_manager(ctx.editor).references(file_path: file_path, line: line, character: character)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def handle_lsp_completion(ctx)
|
|
288
|
+
file_path = ctx.buffer.file_path
|
|
289
|
+
unless file_path
|
|
290
|
+
ctx.set_message("LSP: no file path")
|
|
291
|
+
return
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
line = ctx.window.cursor_row
|
|
295
|
+
character = ctx.window.cursor_col
|
|
296
|
+
get_manager(ctx.editor).completion(file_path: file_path, line: line, character: character)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def handle_lsp_diagnostics(ctx)
|
|
300
|
+
file_path = ctx.buffer.file_path
|
|
301
|
+
uri = file_path ? TextDocumentSync.path_to_uri(file_path) : nil
|
|
302
|
+
mgr = get_manager(ctx.editor)
|
|
303
|
+
|
|
304
|
+
diagnostics = uri ? mgr.diagnostics_handler.diagnostics_for(uri) : []
|
|
305
|
+
|
|
306
|
+
if diagnostics.empty?
|
|
307
|
+
ctx.set_message("LSP: no diagnostics")
|
|
308
|
+
return
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
ctx.set_message("LSP: #{diagnostics.length} diagnostics")
|
|
312
|
+
diagnostics.first(5).each do |d|
|
|
313
|
+
line = d.range.start.line + 1
|
|
314
|
+
ctx.set_message(" [#{d.severity_name}] Line #{line}: #{d.message}")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
return unless diagnostics.length > 5
|
|
318
|
+
|
|
319
|
+
ctx.set_message(" ... and #{diagnostics.length - 5} more")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def handle_lsp_diagnostic_show(ctx)
|
|
323
|
+
file_path = ctx.buffer.file_path
|
|
324
|
+
uri = file_path ? TextDocumentSync.path_to_uri(file_path) : nil
|
|
325
|
+
mgr = get_manager(ctx.editor)
|
|
326
|
+
|
|
327
|
+
return ctx.set_message("LSP: no file") unless uri
|
|
328
|
+
|
|
329
|
+
line = ctx.window.cursor_row
|
|
330
|
+
diagnostics = mgr.diagnostics_handler.diagnostics_at_line(uri, line)
|
|
331
|
+
|
|
332
|
+
if diagnostics.empty?
|
|
333
|
+
ctx.set_message("LSP: no diagnostic at cursor")
|
|
334
|
+
return
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Format diagnostics for display
|
|
338
|
+
lines = diagnostics.map do |d|
|
|
339
|
+
"[#{d.severity_name}] #{d.message}"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Use floating window if available
|
|
343
|
+
if ctx.editor.respond_to?(:show_floating)
|
|
344
|
+
ctx.editor.show_floating(lines.join("\n\n"), max_height: 10)
|
|
345
|
+
else
|
|
346
|
+
ctx.set_message(lines.first)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def handle_lsp_debug(ctx)
|
|
351
|
+
mgr = get_manager(ctx.editor)
|
|
352
|
+
info = mgr.debug_info
|
|
353
|
+
|
|
354
|
+
if info.empty?
|
|
355
|
+
ctx.set_message("LSP Debug: no clients")
|
|
356
|
+
return
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
info.each do |name, data|
|
|
360
|
+
status = if data[:initialized]
|
|
361
|
+
"ready"
|
|
362
|
+
elsif data[:started]
|
|
363
|
+
"starting"
|
|
364
|
+
else
|
|
365
|
+
"stopped"
|
|
366
|
+
end
|
|
367
|
+
ctx.set_message("LSP #{name}: #{status}")
|
|
368
|
+
next unless data[:last_stderr]
|
|
369
|
+
|
|
370
|
+
data[:last_stderr].split("\n").each do |line|
|
|
371
|
+
ctx.set_message(" #{line}")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def handle_lsp_log(ctx)
|
|
377
|
+
mgr = get_manager(ctx.editor)
|
|
378
|
+
info = mgr.debug_info
|
|
379
|
+
|
|
380
|
+
lines = ["=== LSP Log ===", ""]
|
|
381
|
+
|
|
382
|
+
if info.empty?
|
|
383
|
+
lines << "No LSP clients running."
|
|
384
|
+
else
|
|
385
|
+
info.each do |name, data|
|
|
386
|
+
status = if data[:initialized]
|
|
387
|
+
"ready"
|
|
388
|
+
elsif data[:started]
|
|
389
|
+
"starting"
|
|
390
|
+
else
|
|
391
|
+
"stopped"
|
|
392
|
+
end
|
|
393
|
+
lines << "--- #{name} (#{status}) ---"
|
|
394
|
+
if data[:last_stderr]
|
|
395
|
+
lines.concat(data[:last_stderr].split("\n"))
|
|
396
|
+
else
|
|
397
|
+
lines << "(no stderr output)"
|
|
398
|
+
end
|
|
399
|
+
lines << ""
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Add notification log
|
|
404
|
+
lines << "=== Notifications ==="
|
|
405
|
+
notifications = mgr.notification_log
|
|
406
|
+
if notifications.empty?
|
|
407
|
+
lines << "(no notifications received)"
|
|
408
|
+
else
|
|
409
|
+
notifications.each do |n|
|
|
410
|
+
lines << "#{n[:time].strftime("%H:%M:%S")} #{n[:method]}"
|
|
411
|
+
if n[:params]
|
|
412
|
+
params_str = n[:params].to_s
|
|
413
|
+
lines << " #{params_str[0, 200]}#{"..." if params_str.length > 200}"
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
ctx.editor.open_scratch_buffer("[LSP Log]", lines.join("\n"))
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def handle_lsp_open(ctx)
|
|
422
|
+
file_path = ctx.buffer.file_path
|
|
423
|
+
unless file_path
|
|
424
|
+
ctx.set_message("LSP: no file path")
|
|
425
|
+
return
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
text = ctx.buffer.lines.join("\n")
|
|
429
|
+
mgr = get_manager(ctx.editor)
|
|
430
|
+
|
|
431
|
+
# Debug: check if text_sync exists
|
|
432
|
+
text_sync = mgr.text_sync_for(file_path)
|
|
433
|
+
unless text_sync
|
|
434
|
+
ctx.set_message("LSP: no text_sync for #{File.basename(file_path)}")
|
|
435
|
+
return
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
mgr.did_open(file_path: file_path, text: text)
|
|
439
|
+
ctx.set_message("LSP: opened #{File.basename(file_path)}")
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
public
|
|
443
|
+
|
|
444
|
+
def get_manager(editor)
|
|
445
|
+
@managers ||= {}.compare_by_identity
|
|
446
|
+
@managers[editor] ||= create_manager(editor)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def register_server(config)
|
|
450
|
+
@default_server_configs ||= []
|
|
451
|
+
@default_server_configs << config
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def use_server(name)
|
|
455
|
+
config = case name.to_sym
|
|
456
|
+
when :solargraph
|
|
457
|
+
ServerConfig.solargraph(auto_start: true)
|
|
458
|
+
when :ruby_lsp
|
|
459
|
+
ServerConfig.ruby_lsp(auto_start: true)
|
|
460
|
+
when :rubocop
|
|
461
|
+
ServerConfig.rubocop_lsp(auto_start: true)
|
|
462
|
+
when :kanayago
|
|
463
|
+
ServerConfig.kanayago(auto_start: true)
|
|
464
|
+
else
|
|
465
|
+
raise ArgumentError, "Unknown server: #{name}. Use :solargraph, :ruby_lsp, :rubocop, or :kanayago"
|
|
466
|
+
end
|
|
467
|
+
register_server(config)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
def create_manager(editor)
|
|
473
|
+
mgr = Manager.new(editor: editor)
|
|
474
|
+
# Register configured servers
|
|
475
|
+
@default_server_configs&.each do |config|
|
|
476
|
+
mgr.register_server(config)
|
|
477
|
+
end
|
|
478
|
+
mgr
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Register the plugin
|
|
485
|
+
Mui.plugin_manager.register(:lsp, Mui::Lsp::Plugin)
|
|
486
|
+
|
|
487
|
+
# DSL for .muirc configuration
|
|
488
|
+
module Mui
|
|
489
|
+
class << self
|
|
490
|
+
def lsp(&block)
|
|
491
|
+
@lsp_config ||= Lsp::ConfigDsl.new
|
|
492
|
+
@lsp_config.instance_eval(&block) if block
|
|
493
|
+
@lsp_config
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def lsp_server_configs
|
|
497
|
+
@lsp_config&.server_configs || []
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
module Lsp
|
|
502
|
+
# DSL class for configuring LSP in .muirc
|
|
503
|
+
class ConfigDsl
|
|
504
|
+
attr_reader :server_configs
|
|
505
|
+
|
|
506
|
+
def initialize
|
|
507
|
+
@server_configs = []
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def use(name, sync_on_change: nil)
|
|
511
|
+
config = case name.to_sym
|
|
512
|
+
when :solargraph
|
|
513
|
+
ServerConfig.solargraph(auto_start: true)
|
|
514
|
+
when :ruby_lsp
|
|
515
|
+
# ruby_lsp defaults to sync_on_change: false
|
|
516
|
+
ServerConfig.ruby_lsp(auto_start: true, sync_on_change: sync_on_change.nil? ? false : sync_on_change)
|
|
517
|
+
when :rubocop
|
|
518
|
+
ServerConfig.rubocop_lsp(auto_start: true)
|
|
519
|
+
when :kanayago
|
|
520
|
+
ServerConfig.kanayago(auto_start: true)
|
|
521
|
+
else
|
|
522
|
+
raise ArgumentError, "Unknown server: #{name}. Use :solargraph, :ruby_lsp, :rubocop, or :kanayago"
|
|
523
|
+
end
|
|
524
|
+
@server_configs << config
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def server(name:, command:, language_ids:, file_patterns:, auto_start: true, sync_on_change: true)
|
|
528
|
+
@server_configs << ServerConfig.custom(
|
|
529
|
+
name: name,
|
|
530
|
+
command: command,
|
|
531
|
+
language_ids: language_ids,
|
|
532
|
+
file_patterns: file_patterns,
|
|
533
|
+
auto_start: auto_start,
|
|
534
|
+
sync_on_change: sync_on_change
|
|
535
|
+
)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Lsp
|
|
5
|
+
module Protocol
|
|
6
|
+
# LSP DiagnosticSeverity constants
|
|
7
|
+
module DiagnosticSeverity
|
|
8
|
+
ERROR = 1
|
|
9
|
+
WARNING = 2
|
|
10
|
+
INFORMATION = 3
|
|
11
|
+
HINT = 4
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# LSP Diagnostic (error/warning/info/hint message)
|
|
15
|
+
class Diagnostic
|
|
16
|
+
attr_accessor :range, :severity, :code, :source, :message, :related_information
|
|
17
|
+
|
|
18
|
+
def initialize(range:, message:, severity: nil, code: nil, source: nil, related_information: nil)
|
|
19
|
+
@range = range.is_a?(Range) ? range : Range.from_hash(range)
|
|
20
|
+
@message = message
|
|
21
|
+
@severity = severity
|
|
22
|
+
@code = code
|
|
23
|
+
@source = source
|
|
24
|
+
@related_information = related_information
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
result = {
|
|
29
|
+
range: @range.to_h,
|
|
30
|
+
message: @message
|
|
31
|
+
}
|
|
32
|
+
result[:severity] = @severity if @severity
|
|
33
|
+
result[:code] = @code if @code
|
|
34
|
+
result[:source] = @source if @source
|
|
35
|
+
result[:relatedInformation] = @related_information if @related_information
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.from_hash(hash)
|
|
40
|
+
new(
|
|
41
|
+
range: hash["range"] || hash[:range],
|
|
42
|
+
message: hash["message"] || hash[:message],
|
|
43
|
+
severity: hash["severity"] || hash[:severity],
|
|
44
|
+
code: hash["code"] || hash[:code],
|
|
45
|
+
source: hash["source"] || hash[:source],
|
|
46
|
+
related_information: hash["relatedInformation"] || hash[:relatedInformation]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def error?
|
|
51
|
+
@severity == DiagnosticSeverity::ERROR
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def warning?
|
|
55
|
+
@severity == DiagnosticSeverity::WARNING
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def information?
|
|
59
|
+
@severity == DiagnosticSeverity::INFORMATION
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def hint?
|
|
63
|
+
@severity == DiagnosticSeverity::HINT
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def severity_name
|
|
67
|
+
case @severity
|
|
68
|
+
when DiagnosticSeverity::ERROR then "Error"
|
|
69
|
+
when DiagnosticSeverity::WARNING then "Warning"
|
|
70
|
+
when DiagnosticSeverity::INFORMATION then "Information"
|
|
71
|
+
when DiagnosticSeverity::HINT then "Hint"
|
|
72
|
+
else "Unknown"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ==(other)
|
|
77
|
+
return false unless other.is_a?(Diagnostic)
|
|
78
|
+
|
|
79
|
+
@range == other.range &&
|
|
80
|
+
@message == other.message &&
|
|
81
|
+
@severity == other.severity &&
|
|
82
|
+
@code == other.code &&
|
|
83
|
+
@source == other.source
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Mui
|
|
6
|
+
module Lsp
|
|
7
|
+
module Protocol
|
|
8
|
+
# LSP Location (URI and range)
|
|
9
|
+
class Location
|
|
10
|
+
attr_accessor :uri, :range
|
|
11
|
+
|
|
12
|
+
def initialize(uri:, range:)
|
|
13
|
+
@uri = uri
|
|
14
|
+
@range = range.is_a?(Range) ? range : Range.from_hash(range)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{ uri: @uri, range: @range.to_h }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_hash(hash)
|
|
22
|
+
new(
|
|
23
|
+
uri: hash["uri"] || hash[:uri],
|
|
24
|
+
range: hash["range"] || hash[:range]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def file_path
|
|
29
|
+
return nil unless @uri&.start_with?("file://")
|
|
30
|
+
|
|
31
|
+
URI.decode_www_form_component(@uri.sub("file://", ""))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
return false unless other.is_a?(Location)
|
|
36
|
+
|
|
37
|
+
@uri == other.uri && @range == other.range
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Lsp
|
|
5
|
+
module Protocol
|
|
6
|
+
# LSP Position (0-indexed line and character)
|
|
7
|
+
class Position
|
|
8
|
+
attr_accessor :line, :character
|
|
9
|
+
|
|
10
|
+
def initialize(line:, character:)
|
|
11
|
+
@line = line
|
|
12
|
+
@character = character
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ line: @line, character: @character }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_hash(hash)
|
|
20
|
+
new(line: hash["line"] || hash[:line], character: hash["character"] || hash[:character])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
return false unless other.is_a?(Position)
|
|
25
|
+
|
|
26
|
+
@line == other.line && @character == other.character
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|