mui-lsp 0.2.0 → 0.3.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 +4 -4
- data/.rubocop_todo.yml +28 -10
- data/CHANGELOG.md +49 -0
- data/README.md +67 -12
- data/lib/mui/lsp/client.rb +23 -0
- data/lib/mui/lsp/handlers/completion.rb +1 -1
- data/lib/mui/lsp/handlers/definition.rb +13 -6
- data/lib/mui/lsp/handlers/formatting.rb +99 -0
- data/lib/mui/lsp/handlers/references.rb +44 -31
- data/lib/mui/lsp/handlers/type_definition.rb +106 -0
- data/lib/mui/lsp/handlers.rb +2 -0
- data/lib/mui/lsp/manager.rb +218 -10
- data/lib/mui/lsp/plugin.rb +171 -59
- data/lib/mui/lsp/server_config.rb +22 -0
- data/lib/mui/lsp/text_document_sync.rb +2 -0
- data/lib/mui/lsp/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 263fd9cae12708ea5be41993083794357068d8af592bc9b7684f650cd9eb094d
|
|
4
|
+
data.tar.gz: 14d542c5978a50e0095acd951a418e43445208a7907024df7eb4dff8ead2e499
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 488392ecc54c123a1a558def0e6b8ec326f85252f0ab95bb2dc97fc40f5a864e43a60563578d0e3b817207ae5122bcb1f5350a0cb884db5ea01549a9e42bfcf1
|
|
7
|
+
data.tar.gz: 8fd6da70ce1e5640e35f72d3d1239d0ec2d5fb76a3e5791a8b406f0efb1b3a3369a16bed112b1ba153d8ec9e96141fdcc0e6a3483470f2faeed10951864b6e26
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2025-12-
|
|
3
|
+
# on 2025-12-26 06:52:41 UTC using RuboCop version 1.81.7.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -12,36 +12,54 @@ Lint/DuplicateBranch:
|
|
|
12
12
|
Exclude:
|
|
13
13
|
- 'lib/mui/lsp/highlighters/diagnostic_highlighter.rb'
|
|
14
14
|
|
|
15
|
-
# Offense count:
|
|
15
|
+
# Offense count: 1
|
|
16
|
+
# Configuration parameters: AllowComments.
|
|
17
|
+
Lint/EmptyClass:
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'test/mui/lsp/handlers/test_completion.rb'
|
|
20
|
+
|
|
21
|
+
# Offense count: 25
|
|
16
22
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
17
23
|
Metrics/AbcSize:
|
|
18
|
-
Max:
|
|
24
|
+
Max: 46
|
|
19
25
|
|
|
20
|
-
# Offense count:
|
|
26
|
+
# Offense count: 8
|
|
21
27
|
# Configuration parameters: CountComments, CountAsOne.
|
|
22
28
|
Metrics/ClassLength:
|
|
23
|
-
Max:
|
|
29
|
+
Max: 435
|
|
24
30
|
|
|
25
|
-
# Offense count:
|
|
31
|
+
# Offense count: 8
|
|
26
32
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
27
33
|
Metrics/CyclomaticComplexity:
|
|
28
34
|
Max: 26
|
|
29
35
|
|
|
30
|
-
# Offense count:
|
|
36
|
+
# Offense count: 83
|
|
31
37
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
32
38
|
Metrics/MethodLength:
|
|
33
|
-
Max:
|
|
39
|
+
Max: 56
|
|
34
40
|
|
|
35
41
|
# Offense count: 4
|
|
36
42
|
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
|
|
37
43
|
Metrics/ParameterLists:
|
|
38
44
|
Max: 6
|
|
39
45
|
|
|
40
|
-
# Offense count:
|
|
46
|
+
# Offense count: 5
|
|
41
47
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
42
48
|
Metrics/PerceivedComplexity:
|
|
43
49
|
Max: 14
|
|
44
50
|
|
|
51
|
+
# Offense count: 1
|
|
52
|
+
Naming/AccessorMethodName:
|
|
53
|
+
Exclude:
|
|
54
|
+
- 'lib/mui/lsp/handlers/completion.rb'
|
|
55
|
+
|
|
56
|
+
# Offense count: 4
|
|
57
|
+
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
58
|
+
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
59
|
+
Naming/MethodParameterName:
|
|
60
|
+
Exclude:
|
|
61
|
+
- 'test/fixtures/sample.rb'
|
|
62
|
+
|
|
45
63
|
# Offense count: 2
|
|
46
64
|
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
|
47
65
|
# AllowedMethods: call
|
|
@@ -65,7 +83,7 @@ Style/IfUnlessModifier:
|
|
|
65
83
|
Exclude:
|
|
66
84
|
- 'lib/mui/lsp/json_rpc_io.rb'
|
|
67
85
|
|
|
68
|
-
# Offense count:
|
|
86
|
+
# Offense count: 4
|
|
69
87
|
# This cop supports safe autocorrection (--autocorrect).
|
|
70
88
|
# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
|
71
89
|
# URISchemes: http, https
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-12-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Multiple LSP result merging:
|
|
7
|
+
- Definition, References, and Type Definition now query all matching LSP servers
|
|
8
|
+
- Results from all servers are merged and deduplicated
|
|
9
|
+
- Enables showing both Ruby source and RBS type definitions together
|
|
10
|
+
- Steep pre-configured server:
|
|
11
|
+
- `use :steep` in `Mui.lsp` block to enable
|
|
12
|
+
- Requires `Steepfile` in project root
|
|
13
|
+
- Supports `.rb` and `.rbs` files
|
|
14
|
+
- Ruby/RBS file toggle (`<Space>tf`):
|
|
15
|
+
- In Ruby files: jumps to corresponding RBS file in `sig/` directory
|
|
16
|
+
- In RBS files: jumps to corresponding Ruby file in `lib/` directory
|
|
17
|
+
- Searches multiple path patterns (e.g., `lib/mui/config.rb` → `sig/mui/config.rbs`)
|
|
18
|
+
- For non-Ruby files: uses LSP `textDocument/typeDefinition` as before
|
|
19
|
+
- Type definition support (`textDocument/typeDefinition`):
|
|
20
|
+
- `:LspTypeDefinition` command to jump to type definition
|
|
21
|
+
- `<Space>tf` keymap for type definition
|
|
22
|
+
- Works with TypeProf, Sorbet, Steep and other Ruby type checkers
|
|
23
|
+
- Uses same picker UI as Definition when multiple candidates found
|
|
24
|
+
- TypeProf pre-configured server:
|
|
25
|
+
- `use :typeprof` in `Mui.lsp` block to enable
|
|
26
|
+
- Requires `typeprof.conf.jsonc` in project root (run `typeprof --init` to create)
|
|
27
|
+
- Format support (`textDocument/formatting`):
|
|
28
|
+
- `:LspFormat` command to format current file
|
|
29
|
+
- `<Space>ff` keymap for formatting
|
|
30
|
+
- Works with RuboCop LSP and other formatters
|
|
31
|
+
- Location picker for Definition and References:
|
|
32
|
+
- When multiple definitions or references are found, opens a scratch buffer picker
|
|
33
|
+
- `j`/`k` - Navigate up/down (native cursor movement)
|
|
34
|
+
- `\` + `Enter` - Open selected location in current window
|
|
35
|
+
- `Ctrl+t` - Open selected location in new tab
|
|
36
|
+
- `\q` / `\` + `Esc` - Close picker
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Type definition now checks `typeDefinitionProvider` capability before sending requests
|
|
40
|
+
- Prevents sending requests to servers that don't support type definition (e.g., Solargraph)
|
|
41
|
+
- Shows helpful message when no server supports type definition
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- Keymaps changed from leader key (`\`) to `<Space>` prefix:
|
|
45
|
+
- `<Space>df` - Go to definition
|
|
46
|
+
- `<Space>rf` - Find references
|
|
47
|
+
- `<Space>hf` - Show hover information
|
|
48
|
+
- `<Space>cf` - Show completion
|
|
49
|
+
- `<Space>ef` - Show diagnostic at cursor
|
|
50
|
+
- Removed manual `@leader_pending` state management in favor of Mui's native multi-key sequence support
|
|
51
|
+
|
|
3
52
|
## [0.2.0] - 2025-12-12
|
|
4
53
|
|
|
5
54
|
### Added
|
data/README.md
CHANGED
|
@@ -4,10 +4,12 @@ LSP (Language Server Protocol) plugin for [Mui](https://github.com/S-H-GAMELINKS
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Hover**: Show documentation for symbol under cursor (`K` or
|
|
8
|
-
- **Go to Definition**: Jump to symbol definition (
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
7
|
+
- **Hover**: Show documentation for symbol under cursor (`K` or `<Space>hf` or `:LspHover`)
|
|
8
|
+
- **Go to Definition**: Jump to symbol definition (`<Space>df` or `:LspDefinition`)
|
|
9
|
+
- **Go to Type Definition**: Jump to type definition or toggle between Ruby/RBS files (`<Space>tf` or `:LspTypeDefinition`)
|
|
10
|
+
- **Find References**: Show all references to symbol (`<Space>rf` or `:LspReferences`)
|
|
11
|
+
- **Completion**: Get code completion suggestions (`<Space>cf` or `:LspCompletion`)
|
|
12
|
+
- **Format**: Format current file with LSP server (`<Space>ff` or `:LspFormat`)
|
|
11
13
|
- **Diagnostics**: Display errors and warnings from LSP server (`:LspDiagnostics`)
|
|
12
14
|
|
|
13
15
|
## Supported Language Servers
|
|
@@ -18,6 +20,8 @@ Pre-configured servers for Ruby:
|
|
|
18
20
|
- **ruby-lsp** - Shopify's Ruby language server
|
|
19
21
|
- **Kanayago** - Realtime Ruby Syntax Check server
|
|
20
22
|
- **RuboCop** (LSP mode) - Ruby linter with LSP support
|
|
23
|
+
- **TypeProf** - Ruby type analysis and inference
|
|
24
|
+
- **Steep** - Ruby type checker with RBS support
|
|
21
25
|
|
|
22
26
|
Custom servers can be configured for other languages.
|
|
23
27
|
|
|
@@ -59,6 +63,42 @@ Available pre-configured servers:
|
|
|
59
63
|
- `:ruby_lsp` - ruby-lsp (Shopify's Ruby LSP)
|
|
60
64
|
- `:rubocop` - RuboCop in LSP mode
|
|
61
65
|
- `:kanayago` - Kanayago (Japanese Ruby LSP)
|
|
66
|
+
- `:typeprof` - TypeProf (Ruby type analysis)
|
|
67
|
+
- `:steep` - Steep (Ruby type checker with RBS)
|
|
68
|
+
|
|
69
|
+
### TypeProf Setup
|
|
70
|
+
|
|
71
|
+
TypeProf requires a configuration file in your project root. Run:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
typeprof --init
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This creates `typeprof.conf.jsonc` with default settings:
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"typeprof_version": "experimental",
|
|
82
|
+
"rbs_dir": "sig/",
|
|
83
|
+
"analysis_unit_dirs": ["lib"]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
TypeProf will only analyze files in the directories specified in `analysis_unit_dirs`. Without this configuration file, TypeProf will not track documents and type definition features will not work.
|
|
88
|
+
|
|
89
|
+
### Steep Setup
|
|
90
|
+
|
|
91
|
+
Steep requires a `Steepfile` in your project root. Create one with:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# Steepfile
|
|
95
|
+
target :lib do
|
|
96
|
+
signature "sig"
|
|
97
|
+
check "lib"
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Place your RBS type definitions in the `sig/` directory.
|
|
62
102
|
|
|
63
103
|
### Custom Server Configuration
|
|
64
104
|
|
|
@@ -126,27 +166,40 @@ To manually start a server:
|
|
|
126
166
|
| `:LspStatus` | Show running and registered servers |
|
|
127
167
|
| `:LspHover` | Show hover information |
|
|
128
168
|
| `:LspDefinition` | Go to definition |
|
|
169
|
+
| `:LspTypeDefinition` | Go to type definition |
|
|
129
170
|
| `:LspReferences` | Find all references |
|
|
130
171
|
| `:LspCompletion` | Show completion menu |
|
|
131
172
|
| `:LspDiagnostics` | Show diagnostics for current file |
|
|
132
173
|
| `:LspDiagnosticShow` | Show diagnostic at cursor in floating window |
|
|
174
|
+
| `:LspFormat` | Format current file |
|
|
133
175
|
| `:LspLog` | Show LSP server logs in a buffer |
|
|
134
176
|
| `:LspDebug` | Show debug information |
|
|
135
177
|
| `:LspOpen` | Manually notify LSP server about current file |
|
|
136
178
|
|
|
137
179
|
### Keymaps
|
|
138
180
|
|
|
139
|
-
Leader key is `\` (backslash).
|
|
140
|
-
|
|
141
181
|
| Key | Mode | Description |
|
|
142
182
|
|-----|------|-------------|
|
|
143
183
|
| `K` | Normal | Show hover information (in floating window) |
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
| `
|
|
184
|
+
| `<Space>df` | Normal | Go to definition |
|
|
185
|
+
| `<Space>tf` | Normal | Go to type definition (Ruby: toggle .rb/.rbs) |
|
|
186
|
+
| `<Space>rf` | Normal | Find references |
|
|
187
|
+
| `<Space>hf` | Normal | Show hover information (alternative to K) |
|
|
188
|
+
| `<Space>cf` | Normal | Show completion |
|
|
189
|
+
| `<Space>ef` | Normal | Show diagnostic at cursor (in floating window) |
|
|
190
|
+
| `<Space>ff` | Normal | Format current file |
|
|
191
|
+
| `Esc` | Normal | Close floating window / picker |
|
|
192
|
+
|
|
193
|
+
#### Location Picker (for Definition/References with multiple candidates)
|
|
194
|
+
|
|
195
|
+
When multiple definitions or references are found, a picker buffer opens. Use standard Vim navigation:
|
|
196
|
+
|
|
197
|
+
| Key | Description |
|
|
198
|
+
|-----|-------------|
|
|
199
|
+
| `j`/`k` | Navigate up/down (native cursor movement) |
|
|
200
|
+
| `\` + `Enter` | Open selected location in current window |
|
|
201
|
+
| `Ctrl+t` | Open selected location in new tab |
|
|
202
|
+
| `\q` / `\` + `Esc` | Close picker |
|
|
150
203
|
|
|
151
204
|
## Architecture
|
|
152
205
|
|
|
@@ -162,9 +215,11 @@ mui-lsp/
|
|
|
162
215
|
base.rb # Base handler class
|
|
163
216
|
hover.rb # Hover response handler
|
|
164
217
|
definition.rb # Definition response handler
|
|
218
|
+
type_definition.rb # Type definition response handler
|
|
165
219
|
references.rb # References response handler
|
|
166
220
|
diagnostics.rb # Diagnostics notification handler
|
|
167
221
|
completion.rb # Completion response handler
|
|
222
|
+
formatting.rb # Formatting response handler
|
|
168
223
|
json_rpc_io.rb # JSON-RPC 2.0 over stdio
|
|
169
224
|
request_manager.rb # Request ID and callback management
|
|
170
225
|
server_config.rb # Server configuration presets
|
data/lib/mui/lsp/client.rb
CHANGED
|
@@ -104,6 +104,13 @@ module Mui
|
|
|
104
104
|
}, &callback)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
def type_definition(uri:, line:, character:, &callback)
|
|
108
|
+
request("textDocument/typeDefinition", {
|
|
109
|
+
textDocument: { uri: uri },
|
|
110
|
+
position: { line: line, character: character }
|
|
111
|
+
}, &callback)
|
|
112
|
+
end
|
|
113
|
+
|
|
107
114
|
def references(uri:, line:, character:, include_declaration: true, &callback)
|
|
108
115
|
request("textDocument/references", {
|
|
109
116
|
textDocument: { uri: uri },
|
|
@@ -119,6 +126,16 @@ module Mui
|
|
|
119
126
|
}, &callback)
|
|
120
127
|
end
|
|
121
128
|
|
|
129
|
+
def formatting(uri:, tab_size: 2, insert_spaces: true, &callback)
|
|
130
|
+
request("textDocument/formatting", {
|
|
131
|
+
textDocument: { uri: uri },
|
|
132
|
+
options: {
|
|
133
|
+
tabSize: tab_size,
|
|
134
|
+
insertSpaces: insert_spaces
|
|
135
|
+
}
|
|
136
|
+
}, &callback)
|
|
137
|
+
end
|
|
138
|
+
|
|
122
139
|
def did_open(uri:, language_id:, version:, text:)
|
|
123
140
|
notify("textDocument/didOpen", {
|
|
124
141
|
textDocument: {
|
|
@@ -270,7 +287,13 @@ module Mui
|
|
|
270
287
|
definition: {
|
|
271
288
|
linkSupport: false
|
|
272
289
|
},
|
|
290
|
+
typeDefinition: {
|
|
291
|
+
linkSupport: false
|
|
292
|
+
},
|
|
273
293
|
references: {},
|
|
294
|
+
formatting: {
|
|
295
|
+
dynamicRegistration: false
|
|
296
|
+
},
|
|
274
297
|
publishDiagnostics: {
|
|
275
298
|
relatedInformation: true
|
|
276
299
|
},
|
|
@@ -83,15 +83,22 @@ module Mui
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def show_location_list(locations)
|
|
86
|
-
#
|
|
87
|
-
|
|
86
|
+
# Store locations for picker navigation
|
|
87
|
+
@editor.instance_variable_set(:@lsp_picker_locations, locations)
|
|
88
|
+
@editor.instance_variable_set(:@lsp_picker_type, :definition)
|
|
89
|
+
|
|
90
|
+
# Build picker content
|
|
91
|
+
lines = []
|
|
92
|
+
locations.each_with_index do |loc, idx|
|
|
88
93
|
file_path = loc.file_path || loc.uri
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
display_path = File.basename(file_path.to_s)
|
|
95
|
+
line_num = loc.range.start.line + 1
|
|
96
|
+
lines << "#{idx + 1}. #{display_path}:#{line_num}"
|
|
91
97
|
end
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
# Open scratch buffer for picker
|
|
100
|
+
content = "Definitions (\\Enter:open, Ctrl+t:tab, \\q:close)\n\n#{lines.join("\n")}"
|
|
101
|
+
@editor.open_scratch_buffer("[LSP Picker]", content)
|
|
95
102
|
end
|
|
96
103
|
end
|
|
97
104
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Lsp
|
|
5
|
+
module Handlers
|
|
6
|
+
# Handler for textDocument/formatting responses
|
|
7
|
+
class Formatting < Base
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def handle_result(result)
|
|
11
|
+
return handle_empty unless result.is_a?(Array) && !result.empty?
|
|
12
|
+
|
|
13
|
+
apply_text_edits(result)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def handle_empty
|
|
17
|
+
@editor.message = "No formatting changes"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def apply_text_edits(edits)
|
|
23
|
+
buffer = @editor.buffer
|
|
24
|
+
return @editor.message = "No buffer" unless buffer
|
|
25
|
+
|
|
26
|
+
# Sort edits in reverse order (bottom to top) to avoid position shifts
|
|
27
|
+
sorted_edits = edits.sort_by do |edit|
|
|
28
|
+
range = edit["range"]
|
|
29
|
+
start_line = range["start"]["line"]
|
|
30
|
+
start_char = range["start"]["character"]
|
|
31
|
+
[-start_line, -start_char]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Apply each edit
|
|
35
|
+
changes_count = 0
|
|
36
|
+
sorted_edits.each do |edit|
|
|
37
|
+
apply_single_edit(buffer, edit)
|
|
38
|
+
changes_count += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@editor.message = "Formatted (#{changes_count} change#{"s" unless changes_count == 1})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_single_edit(buffer, edit)
|
|
45
|
+
range = edit["range"]
|
|
46
|
+
new_text = edit["newText"]
|
|
47
|
+
|
|
48
|
+
start_line = range["start"]["line"]
|
|
49
|
+
start_char = range["start"]["character"]
|
|
50
|
+
end_line = range["end"]["line"]
|
|
51
|
+
end_char = range["end"]["character"]
|
|
52
|
+
|
|
53
|
+
# Get current lines
|
|
54
|
+
lines = buffer.lines
|
|
55
|
+
|
|
56
|
+
# Build new content
|
|
57
|
+
# Get text before the edit range
|
|
58
|
+
before_text = if start_line < lines.length
|
|
59
|
+
line = lines[start_line] || ""
|
|
60
|
+
line[0, start_char] || ""
|
|
61
|
+
else
|
|
62
|
+
""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get text after the edit range
|
|
66
|
+
after_text = if end_line < lines.length
|
|
67
|
+
line = lines[end_line] || ""
|
|
68
|
+
line[end_char..] || ""
|
|
69
|
+
else
|
|
70
|
+
""
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Split new_text into lines
|
|
74
|
+
new_lines = new_text.split("\n", -1)
|
|
75
|
+
|
|
76
|
+
new_lines = [""] if new_lines.empty?
|
|
77
|
+
|
|
78
|
+
# Combine before_text with first new line
|
|
79
|
+
new_lines[0] = before_text + new_lines[0]
|
|
80
|
+
|
|
81
|
+
# Combine last new line with after_text
|
|
82
|
+
new_lines[-1] = new_lines[-1] + after_text
|
|
83
|
+
|
|
84
|
+
# Replace lines in buffer
|
|
85
|
+
# Delete old lines
|
|
86
|
+
delete_count = end_line - start_line + 1
|
|
87
|
+
delete_count.times do
|
|
88
|
+
buffer.delete_line(start_line) if start_line < buffer.lines.length
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Insert new lines
|
|
92
|
+
new_lines.each_with_index do |line, idx|
|
|
93
|
+
buffer.insert_line(start_line + idx, line)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -11,7 +11,12 @@ module Mui
|
|
|
11
11
|
return handle_empty unless result.is_a?(Array) && !result.empty?
|
|
12
12
|
|
|
13
13
|
locations = result.map { |loc| Protocol::Location.from_hash(loc) }
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
if locations.length == 1
|
|
16
|
+
jump_to_location(locations.first)
|
|
17
|
+
else
|
|
18
|
+
show_location_list(locations)
|
|
19
|
+
end
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def handle_empty
|
|
@@ -20,44 +25,52 @@ module Mui
|
|
|
20
25
|
|
|
21
26
|
private
|
|
22
27
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# Group by file for display
|
|
30
|
-
by_file = locations.group_by(&:file_path)
|
|
31
|
-
|
|
32
|
-
# Display first few references
|
|
33
|
-
displayed = 0
|
|
34
|
-
max_display = 3
|
|
35
|
-
|
|
36
|
-
by_file.each do |file_path, file_locations|
|
|
37
|
-
break if displayed >= max_display
|
|
28
|
+
def jump_to_location(location)
|
|
29
|
+
file_path = location.file_path
|
|
30
|
+
unless file_path
|
|
31
|
+
@editor.message = "Cannot open: #{location.uri}"
|
|
32
|
+
return
|
|
33
|
+
end
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
line = location.range.start.line
|
|
36
|
+
character = location.range.start.character
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
# Open the file in current window
|
|
39
|
+
current_buffer = @editor.buffer
|
|
40
|
+
if current_buffer.file_path != file_path
|
|
41
|
+
new_buffer = Mui::Buffer.new
|
|
42
|
+
new_buffer.load(file_path)
|
|
43
|
+
@editor.window.buffer = new_buffer
|
|
46
44
|
end
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
# Jump to position
|
|
47
|
+
window = @editor.window
|
|
48
|
+
return unless window
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
window.cursor_row = line
|
|
51
|
+
window.cursor_col = character
|
|
52
|
+
window.ensure_cursor_visible
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
# Store references for navigation
|
|
54
|
-
store_references(locations)
|
|
54
|
+
@editor.message = "#{File.basename(file_path)}:#{line + 1}"
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def
|
|
58
|
-
# Store
|
|
59
|
-
|
|
60
|
-
@editor.instance_variable_set(:@
|
|
57
|
+
def show_location_list(locations)
|
|
58
|
+
# Store locations for picker navigation
|
|
59
|
+
@editor.instance_variable_set(:@lsp_picker_locations, locations)
|
|
60
|
+
@editor.instance_variable_set(:@lsp_picker_type, :references)
|
|
61
|
+
|
|
62
|
+
# Build picker content
|
|
63
|
+
lines = []
|
|
64
|
+
locations.each_with_index do |loc, idx|
|
|
65
|
+
file_path = loc.file_path || loc.uri
|
|
66
|
+
display_path = File.basename(file_path.to_s)
|
|
67
|
+
line_num = loc.range.start.line + 1
|
|
68
|
+
lines << "#{idx + 1}. #{display_path}:#{line_num}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Open scratch buffer for picker
|
|
72
|
+
content = "References (#{locations.length} found) (\\Enter:open, Ctrl+t:tab, \\q:close)\n\n#{lines.join("\n")}"
|
|
73
|
+
@editor.open_scratch_buffer("[LSP Picker]", content)
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Lsp
|
|
5
|
+
module Handlers
|
|
6
|
+
# Handler for textDocument/typeDefinition responses
|
|
7
|
+
class TypeDefinition < Base
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def handle_result(result)
|
|
11
|
+
locations = normalize_locations(result)
|
|
12
|
+
return handle_empty if locations.empty?
|
|
13
|
+
|
|
14
|
+
if locations.length == 1
|
|
15
|
+
jump_to_location(locations.first)
|
|
16
|
+
else
|
|
17
|
+
show_location_list(locations)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def handle_empty
|
|
22
|
+
@editor.message = "No type definition found"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def normalize_locations(result)
|
|
28
|
+
case result
|
|
29
|
+
when Array
|
|
30
|
+
result.map { |loc| parse_location(loc) }.compact
|
|
31
|
+
when Hash
|
|
32
|
+
location = parse_location(result)
|
|
33
|
+
location ? [location] : []
|
|
34
|
+
else
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_location(data)
|
|
40
|
+
return nil unless data
|
|
41
|
+
|
|
42
|
+
# Handle both Location and LocationLink
|
|
43
|
+
if data["targetUri"]
|
|
44
|
+
# LocationLink
|
|
45
|
+
Protocol::Location.new(
|
|
46
|
+
uri: data["targetUri"],
|
|
47
|
+
range: data["targetSelectionRange"] || data["targetRange"]
|
|
48
|
+
)
|
|
49
|
+
elsif data["uri"]
|
|
50
|
+
# Location
|
|
51
|
+
Protocol::Location.from_hash(data)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def jump_to_location(location)
|
|
56
|
+
file_path = location.file_path
|
|
57
|
+
unless file_path
|
|
58
|
+
@editor.message = "Cannot open: #{location.uri}"
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
line = location.range.start.line
|
|
63
|
+
character = location.range.start.character
|
|
64
|
+
|
|
65
|
+
# Open the file in current window
|
|
66
|
+
current_buffer = @editor.buffer
|
|
67
|
+
if current_buffer.file_path != file_path
|
|
68
|
+
# Need to open a different file
|
|
69
|
+
new_buffer = Mui::Buffer.new
|
|
70
|
+
new_buffer.load(file_path)
|
|
71
|
+
@editor.window.buffer = new_buffer
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Jump to position
|
|
75
|
+
window = @editor.window
|
|
76
|
+
return unless window
|
|
77
|
+
|
|
78
|
+
window.cursor_row = line
|
|
79
|
+
window.cursor_col = character
|
|
80
|
+
window.ensure_cursor_visible
|
|
81
|
+
|
|
82
|
+
@editor.message = "#{File.basename(file_path)}:#{line + 1}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def show_location_list(locations)
|
|
86
|
+
# Store locations for picker navigation
|
|
87
|
+
@editor.instance_variable_set(:@lsp_picker_locations, locations)
|
|
88
|
+
@editor.instance_variable_set(:@lsp_picker_type, :type_definition)
|
|
89
|
+
|
|
90
|
+
# Build picker content
|
|
91
|
+
lines = []
|
|
92
|
+
locations.each_with_index do |loc, idx|
|
|
93
|
+
file_path = loc.file_path || loc.uri
|
|
94
|
+
display_path = File.basename(file_path.to_s)
|
|
95
|
+
line_num = loc.range.start.line + 1
|
|
96
|
+
lines << "#{idx + 1}. #{display_path}:#{line_num}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Open scratch buffer for picker
|
|
100
|
+
content = "Type Definitions (\\Enter:open, Ctrl+t:tab, \\q:close)\n\n#{lines.join("\n")}"
|
|
101
|
+
@editor.open_scratch_buffer("[LSP Picker]", content)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/mui/lsp/handlers.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "handlers/base"
|
|
4
4
|
require_relative "handlers/hover"
|
|
5
5
|
require_relative "handlers/definition"
|
|
6
|
+
require_relative "handlers/type_definition"
|
|
6
7
|
require_relative "handlers/references"
|
|
7
8
|
require_relative "handlers/diagnostics"
|
|
8
9
|
require_relative "handlers/completion"
|
|
10
|
+
require_relative "handlers/formatting"
|
data/lib/mui/lsp/manager.rb
CHANGED
|
@@ -86,6 +86,20 @@ module Mui
|
|
|
86
86
|
nil
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
def client_for_capability(file_path, capability)
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
@server_configs.each do |name, config|
|
|
92
|
+
next unless config.handles_file?(file_path)
|
|
93
|
+
next unless @clients[name]&.running?
|
|
94
|
+
|
|
95
|
+
# Check if server supports the capability
|
|
96
|
+
capabilities = @clients[name].server_capabilities
|
|
97
|
+
return @clients[name] if capabilities[capability]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
89
103
|
def text_sync_for(file_path)
|
|
90
104
|
@mutex.synchronize do
|
|
91
105
|
@server_configs.each do |name, config|
|
|
@@ -215,21 +229,106 @@ module Mui
|
|
|
215
229
|
end
|
|
216
230
|
|
|
217
231
|
def definition(file_path:, line:, character:)
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
text_syncs = text_syncs_for(file_path)
|
|
233
|
+
if text_syncs.empty?
|
|
220
234
|
@editor.message = server_unavailable_message(file_path)
|
|
221
235
|
return
|
|
222
236
|
end
|
|
223
237
|
|
|
224
238
|
uri = TextDocumentSync.path_to_uri(file_path)
|
|
225
|
-
handler = Handlers::Definition.new(editor: @editor, client: client)
|
|
239
|
+
handler = Handlers::Definition.new(editor: @editor, client: text_syncs.first.client)
|
|
240
|
+
|
|
241
|
+
# Collect results from all clients
|
|
242
|
+
results_mutex = Mutex.new
|
|
243
|
+
pending_count = text_syncs.size
|
|
244
|
+
all_results = []
|
|
245
|
+
|
|
246
|
+
text_syncs.each do |text_sync|
|
|
247
|
+
text_sync.client.definition(uri: uri, line: line, character: character) do |result, _error|
|
|
248
|
+
results_mutex.synchronize do
|
|
249
|
+
all_results << result if result
|
|
250
|
+
pending_count -= 1
|
|
251
|
+
|
|
252
|
+
if pending_count.zero?
|
|
253
|
+
merged = merge_locations(all_results)
|
|
254
|
+
handler.handle(merged, nil)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
226
260
|
|
|
227
|
-
|
|
228
|
-
|
|
261
|
+
def type_definition(file_path:, line:, character:)
|
|
262
|
+
# Only send to servers that support typeDefinitionProvider
|
|
263
|
+
text_syncs = text_syncs_for(file_path).select do |ts|
|
|
264
|
+
ts.client.server_capabilities["typeDefinitionProvider"]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if text_syncs.empty?
|
|
268
|
+
# Fallback message: check if any server is running but doesn't support typeDefinition
|
|
269
|
+
any_text_sync = text_syncs_for(file_path).first
|
|
270
|
+
@editor.message = if any_text_sync
|
|
271
|
+
"LSP: no server supports typeDefinition for this file"
|
|
272
|
+
else
|
|
273
|
+
server_unavailable_message(file_path)
|
|
274
|
+
end
|
|
275
|
+
return
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
uri = TextDocumentSync.path_to_uri(file_path)
|
|
279
|
+
handler = Handlers::TypeDefinition.new(editor: @editor, client: text_syncs.first.client)
|
|
280
|
+
|
|
281
|
+
# Collect results from all clients
|
|
282
|
+
results_mutex = Mutex.new
|
|
283
|
+
pending_count = text_syncs.size
|
|
284
|
+
all_results = []
|
|
285
|
+
|
|
286
|
+
text_syncs.each do |text_sync|
|
|
287
|
+
text_sync.client.type_definition(uri: uri, line: line, character: character) do |result, _error|
|
|
288
|
+
results_mutex.synchronize do
|
|
289
|
+
all_results << result if result
|
|
290
|
+
pending_count -= 1
|
|
291
|
+
|
|
292
|
+
if pending_count.zero?
|
|
293
|
+
merged = merge_locations(all_results)
|
|
294
|
+
handler.handle(merged, nil)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
229
298
|
end
|
|
230
299
|
end
|
|
231
300
|
|
|
232
301
|
def references(file_path:, line:, character:)
|
|
302
|
+
text_syncs = text_syncs_for(file_path)
|
|
303
|
+
if text_syncs.empty?
|
|
304
|
+
@editor.message = server_unavailable_message(file_path)
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
uri = TextDocumentSync.path_to_uri(file_path)
|
|
309
|
+
handler = Handlers::References.new(editor: @editor, client: text_syncs.first.client)
|
|
310
|
+
|
|
311
|
+
# Collect results from all clients
|
|
312
|
+
results_mutex = Mutex.new
|
|
313
|
+
pending_count = text_syncs.size
|
|
314
|
+
all_results = []
|
|
315
|
+
|
|
316
|
+
text_syncs.each do |text_sync|
|
|
317
|
+
text_sync.client.references(uri: uri, line: line, character: character) do |result, _error|
|
|
318
|
+
results_mutex.synchronize do
|
|
319
|
+
all_results << result if result
|
|
320
|
+
pending_count -= 1
|
|
321
|
+
|
|
322
|
+
if pending_count.zero?
|
|
323
|
+
merged = merge_locations(all_results)
|
|
324
|
+
handler.handle(merged, nil)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def completion(file_path:, line:, character:)
|
|
233
332
|
client = client_for(file_path)
|
|
234
333
|
unless client
|
|
235
334
|
@editor.message = server_unavailable_message(file_path)
|
|
@@ -237,14 +336,14 @@ module Mui
|
|
|
237
336
|
end
|
|
238
337
|
|
|
239
338
|
uri = TextDocumentSync.path_to_uri(file_path)
|
|
240
|
-
handler = Handlers::
|
|
339
|
+
handler = Handlers::Completion.new(editor: @editor, client: client)
|
|
241
340
|
|
|
242
|
-
client.
|
|
341
|
+
client.completion(uri: uri, line: line, character: character) do |result, error|
|
|
243
342
|
handler.handle(result, error)
|
|
244
343
|
end
|
|
245
344
|
end
|
|
246
345
|
|
|
247
|
-
def
|
|
346
|
+
def format(file_path:, tab_size: 2, insert_spaces: true)
|
|
248
347
|
client = client_for(file_path)
|
|
249
348
|
unless client
|
|
250
349
|
@editor.message = server_unavailable_message(file_path)
|
|
@@ -252,13 +351,48 @@ module Mui
|
|
|
252
351
|
end
|
|
253
352
|
|
|
254
353
|
uri = TextDocumentSync.path_to_uri(file_path)
|
|
255
|
-
handler = Handlers::
|
|
354
|
+
handler = Handlers::Formatting.new(editor: @editor, client: client)
|
|
256
355
|
|
|
257
|
-
client.
|
|
356
|
+
client.formatting(uri: uri, tab_size: tab_size, insert_spaces: insert_spaces) do |result, error|
|
|
258
357
|
handler.handle(result, error)
|
|
259
358
|
end
|
|
260
359
|
end
|
|
261
360
|
|
|
361
|
+
def jump_to_type_file(file_path:, line: nil, character: nil)
|
|
362
|
+
# For Ruby/RBS files, use custom toggle behavior
|
|
363
|
+
if file_path&.end_with?(".rb", ".rbs")
|
|
364
|
+
jump_to_ruby_type_file(file_path)
|
|
365
|
+
else
|
|
366
|
+
# For other languages, use LSP typeDefinition
|
|
367
|
+
type_definition(file_path: file_path, line: line, character: character)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
def jump_to_ruby_type_file(file_path)
|
|
374
|
+
target_path = if file_path.end_with?(".rb")
|
|
375
|
+
find_rbs_file(file_path)
|
|
376
|
+
else
|
|
377
|
+
find_ruby_file(file_path)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
unless target_path
|
|
381
|
+
ext = File.extname(file_path)
|
|
382
|
+
target_ext = ext == ".rb" ? ".rbs" : ".rb"
|
|
383
|
+
@editor.message = "No #{target_ext} file found for #{File.basename(file_path)}"
|
|
384
|
+
return
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Open the target file
|
|
388
|
+
new_buffer = Mui::Buffer.new
|
|
389
|
+
new_buffer.load(target_path)
|
|
390
|
+
@editor.window.buffer = new_buffer
|
|
391
|
+
@editor.message = "Opened #{File.basename(target_path)}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
public
|
|
395
|
+
|
|
262
396
|
def running_servers
|
|
263
397
|
@mutex.synchronize do
|
|
264
398
|
@clients.select { |_, client| client.running? }.keys
|
|
@@ -293,6 +427,80 @@ module Mui
|
|
|
293
427
|
|
|
294
428
|
private
|
|
295
429
|
|
|
430
|
+
def merge_locations(results)
|
|
431
|
+
# Flatten all results into a single array
|
|
432
|
+
merged = results.flat_map do |result|
|
|
433
|
+
case result
|
|
434
|
+
when Array then result
|
|
435
|
+
when Hash then [result]
|
|
436
|
+
else []
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Remove duplicates based on uri and range
|
|
441
|
+
merged.uniq do |loc|
|
|
442
|
+
uri = loc["uri"] || loc["targetUri"]
|
|
443
|
+
range = loc["range"] || loc["targetSelectionRange"]
|
|
444
|
+
[uri, range]
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def find_rbs_file(ruby_file_path)
|
|
449
|
+
# Find project root
|
|
450
|
+
project_root = find_project_root(ruby_file_path)
|
|
451
|
+
|
|
452
|
+
# Get relative path from project root
|
|
453
|
+
abs_path = File.expand_path(ruby_file_path)
|
|
454
|
+
rel_path = abs_path.sub("#{project_root}/", "")
|
|
455
|
+
|
|
456
|
+
# Try different RBS path patterns
|
|
457
|
+
candidates = []
|
|
458
|
+
|
|
459
|
+
# Pattern 1: sig/relative_path.rbs (e.g., lib/mui/config.rb -> sig/lib/mui/config.rbs)
|
|
460
|
+
candidates << File.join(project_root, "sig", rel_path.sub(/\.rb$/, ".rbs"))
|
|
461
|
+
|
|
462
|
+
# Pattern 2: sig/without_lib.rbs (e.g., lib/mui/config.rb -> sig/mui/config.rbs)
|
|
463
|
+
if rel_path.start_with?("lib/")
|
|
464
|
+
candidates << File.join(project_root, "sig", rel_path.sub(%r{^lib/}, "").sub(/\.rb$/, ".rbs"))
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Pattern 3: sig/basename.rbs (e.g., lib/mui/config.rb -> sig/config.rbs)
|
|
468
|
+
candidates << File.join(project_root, "sig", "#{File.basename(ruby_file_path, ".rb")}.rbs")
|
|
469
|
+
|
|
470
|
+
# Return first existing file
|
|
471
|
+
candidates.find { |path| File.exist?(path) }
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def find_ruby_file(rbs_file_path)
|
|
475
|
+
# Find project root
|
|
476
|
+
project_root = find_project_root(rbs_file_path)
|
|
477
|
+
|
|
478
|
+
# Get relative path from project root
|
|
479
|
+
abs_path = File.expand_path(rbs_file_path)
|
|
480
|
+
rel_path = abs_path.sub("#{project_root}/", "")
|
|
481
|
+
|
|
482
|
+
# Remove sig/ prefix if present
|
|
483
|
+
rel_path = rel_path.sub(%r{^sig/}, "")
|
|
484
|
+
|
|
485
|
+
# Try different Ruby path patterns
|
|
486
|
+
candidates = []
|
|
487
|
+
|
|
488
|
+
# Pattern 1: lib/relative_path.rb (e.g., sig/mui/config.rbs -> lib/mui/config.rb)
|
|
489
|
+
candidates << File.join(project_root, "lib", rel_path.sub(/\.rbs$/, ".rb"))
|
|
490
|
+
|
|
491
|
+
# Pattern 2: relative_path.rb without lib (e.g., sig/lib/mui/config.rbs -> lib/mui/config.rb)
|
|
492
|
+
candidates << File.join(project_root, rel_path.sub(/\.rbs$/, ".rb")) if rel_path.start_with?("lib/")
|
|
493
|
+
|
|
494
|
+
# Pattern 3: Search in lib directory for basename
|
|
495
|
+
basename = File.basename(rbs_file_path, ".rbs")
|
|
496
|
+
Dir.glob(File.join(project_root, "lib", "**", "#{basename}.rb")).each do |path|
|
|
497
|
+
candidates << path
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Return first existing file
|
|
501
|
+
candidates.find { |path| File.exist?(path) }
|
|
502
|
+
end
|
|
503
|
+
|
|
296
504
|
def send_pending_documents(server_name)
|
|
297
505
|
text_sync = @mutex.synchronize { @text_syncs[server_name] }
|
|
298
506
|
config = @mutex.synchronize { @server_configs[server_name] }
|
data/lib/mui/lsp/plugin.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Mui
|
|
|
6
6
|
module Lsp
|
|
7
7
|
# Main plugin class for mui-lsp
|
|
8
8
|
# Registers commands and keymaps for LSP integration
|
|
9
|
-
class Plugin < Mui::Plugin
|
|
9
|
+
class Plugin < Mui::Plugin # rubocop:disable Metrics/ClassLength
|
|
10
10
|
name "lsp"
|
|
11
11
|
|
|
12
12
|
def setup
|
|
@@ -44,6 +44,11 @@ module Mui
|
|
|
44
44
|
handle_lsp_definition(ctx)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# :LspTypeDefinition - Go to type definition
|
|
48
|
+
command(:LspTypeDefinition) do |ctx, _args|
|
|
49
|
+
handle_lsp_type_definition(ctx)
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
# :LspReferences - Show references
|
|
48
53
|
command(:LspReferences) do |ctx, _args|
|
|
49
54
|
handle_lsp_references(ctx)
|
|
@@ -78,6 +83,11 @@ module Mui
|
|
|
78
83
|
command(:LspOpen) do |ctx, _args|
|
|
79
84
|
handle_lsp_open(ctx)
|
|
80
85
|
end
|
|
86
|
+
|
|
87
|
+
# :LspFormat - Format current file
|
|
88
|
+
command(:LspFormat) do |ctx, _args|
|
|
89
|
+
handle_lsp_format(ctx)
|
|
90
|
+
end
|
|
81
91
|
end
|
|
82
92
|
|
|
83
93
|
def register_keymaps
|
|
@@ -87,75 +97,46 @@ module Mui
|
|
|
87
97
|
true
|
|
88
98
|
end
|
|
89
99
|
|
|
90
|
-
#
|
|
91
|
-
keymap(:normal, "
|
|
92
|
-
|
|
100
|
+
# <Space>df - Go to definition
|
|
101
|
+
keymap(:normal, "<Space>df") do |ctx|
|
|
102
|
+
handle_lsp_definition(ctx)
|
|
93
103
|
true
|
|
94
104
|
end
|
|
95
105
|
|
|
96
|
-
#
|
|
97
|
-
keymap(:normal, "
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
handle_lsp_definition(ctx)
|
|
101
|
-
true
|
|
102
|
-
else
|
|
103
|
-
false # Let default 'd' handle it
|
|
104
|
-
end
|
|
106
|
+
# <Space>tf - Go to type definition (toggle between .rb and .rbs)
|
|
107
|
+
keymap(:normal, "<Space>tf") do |ctx|
|
|
108
|
+
handle_lsp_jump_to_type_file(ctx)
|
|
109
|
+
true
|
|
105
110
|
end
|
|
106
111
|
|
|
107
|
-
#
|
|
108
|
-
keymap(:normal, "
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
handle_lsp_references(ctx)
|
|
112
|
-
true
|
|
113
|
-
else
|
|
114
|
-
false # Let default 'r' handle it
|
|
115
|
-
end
|
|
112
|
+
# <Space>rf - Find references
|
|
113
|
+
keymap(:normal, "<Space>rf") do |ctx|
|
|
114
|
+
handle_lsp_references(ctx)
|
|
115
|
+
true
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
#
|
|
119
|
-
keymap(:normal, "
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
handle_lsp_hover(ctx)
|
|
123
|
-
true
|
|
124
|
-
else
|
|
125
|
-
false # Let default 'h' handle it
|
|
126
|
-
end
|
|
118
|
+
# <Space>hf - Show hover (alternative to K)
|
|
119
|
+
keymap(:normal, "<Space>hf") do |ctx|
|
|
120
|
+
handle_lsp_hover(ctx)
|
|
121
|
+
true
|
|
127
122
|
end
|
|
128
123
|
|
|
129
|
-
#
|
|
130
|
-
keymap(:normal, "
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
handle_lsp_completion(ctx)
|
|
134
|
-
true
|
|
135
|
-
else
|
|
136
|
-
false # Let default 'c' handle it
|
|
137
|
-
end
|
|
124
|
+
# <Space>cf - Show completion
|
|
125
|
+
keymap(:normal, "<Space>cf") do |ctx|
|
|
126
|
+
handle_lsp_completion(ctx)
|
|
127
|
+
true
|
|
138
128
|
end
|
|
139
129
|
|
|
140
|
-
#
|
|
141
|
-
keymap(:normal, "
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
handle_lsp_diagnostic_show(ctx)
|
|
145
|
-
true
|
|
146
|
-
else
|
|
147
|
-
false # Let default 'e' handle it
|
|
148
|
-
end
|
|
130
|
+
# <Space>ef - Show diagnostic at cursor
|
|
131
|
+
keymap(:normal, "<Space>ef") do |ctx|
|
|
132
|
+
handle_lsp_diagnostic_show(ctx)
|
|
133
|
+
true
|
|
149
134
|
end
|
|
150
135
|
|
|
151
|
-
#
|
|
152
|
-
keymap(:normal, "
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
true
|
|
156
|
-
else
|
|
157
|
-
false
|
|
158
|
-
end
|
|
136
|
+
# <Space>ff - Format current file
|
|
137
|
+
keymap(:normal, "<Space>ff") do |ctx|
|
|
138
|
+
handle_lsp_format(ctx)
|
|
139
|
+
true
|
|
159
140
|
end
|
|
160
141
|
|
|
161
142
|
# Insert mode: Ctrl+Space - Trigger LSP completion
|
|
@@ -163,6 +144,38 @@ module Mui
|
|
|
163
144
|
handle_lsp_completion(ctx)
|
|
164
145
|
true
|
|
165
146
|
end
|
|
147
|
+
|
|
148
|
+
# Picker navigation: Leader+Enter - open selected
|
|
149
|
+
keymap(:normal, "<Leader><CR>") do |ctx|
|
|
150
|
+
next unless picker_active?(ctx.editor)
|
|
151
|
+
|
|
152
|
+
handle_picker_select(ctx, new_tab: false)
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Picker navigation: Ctrl+t - open in new tab
|
|
157
|
+
keymap(:normal, "<C-t>") do |ctx|
|
|
158
|
+
next unless picker_active?(ctx.editor)
|
|
159
|
+
|
|
160
|
+
handle_picker_select(ctx, new_tab: true)
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Picker navigation: Leader+q - close picker
|
|
165
|
+
keymap(:normal, "<Leader>q") do |ctx|
|
|
166
|
+
next unless picker_active?(ctx.editor)
|
|
167
|
+
|
|
168
|
+
close_picker(ctx.editor)
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Picker navigation: Leader+Esc - close picker
|
|
173
|
+
keymap(:normal, "<Leader><Esc>") do |ctx|
|
|
174
|
+
next unless picker_active?(ctx.editor)
|
|
175
|
+
|
|
176
|
+
close_picker(ctx.editor)
|
|
177
|
+
true
|
|
178
|
+
end
|
|
166
179
|
end
|
|
167
180
|
|
|
168
181
|
def register_autocmds
|
|
@@ -283,6 +296,18 @@ module Mui
|
|
|
283
296
|
get_manager(ctx.editor).definition(file_path: file_path, line: line, character: character)
|
|
284
297
|
end
|
|
285
298
|
|
|
299
|
+
def handle_lsp_jump_to_type_file(ctx)
|
|
300
|
+
file_path = ctx.buffer.file_path
|
|
301
|
+
unless file_path
|
|
302
|
+
ctx.set_message("LSP: no file path")
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
line = ctx.window.cursor_row
|
|
307
|
+
character = ctx.window.cursor_col
|
|
308
|
+
get_manager(ctx.editor).jump_to_type_file(file_path: file_path, line: line, character: character)
|
|
309
|
+
end
|
|
310
|
+
|
|
286
311
|
def handle_lsp_references(ctx)
|
|
287
312
|
file_path = ctx.buffer.file_path
|
|
288
313
|
unless file_path
|
|
@@ -456,6 +481,22 @@ module Mui
|
|
|
456
481
|
ctx.set_message("LSP: opened #{File.basename(file_path)}")
|
|
457
482
|
end
|
|
458
483
|
|
|
484
|
+
def handle_lsp_format(ctx)
|
|
485
|
+
file_path = ctx.buffer.file_path
|
|
486
|
+
unless file_path
|
|
487
|
+
ctx.set_message("LSP: no file path")
|
|
488
|
+
return
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
mgr = get_manager(ctx.editor)
|
|
492
|
+
text = ctx.buffer.lines.join("\n")
|
|
493
|
+
|
|
494
|
+
# Sync document before formatting
|
|
495
|
+
mgr.sync_now(file_path: file_path, text: text)
|
|
496
|
+
|
|
497
|
+
mgr.format(file_path: file_path)
|
|
498
|
+
end
|
|
499
|
+
|
|
459
500
|
public
|
|
460
501
|
|
|
461
502
|
def get_manager(editor)
|
|
@@ -478,14 +519,80 @@ module Mui
|
|
|
478
519
|
ServerConfig.rubocop_lsp(auto_start: true)
|
|
479
520
|
when :kanayago
|
|
480
521
|
ServerConfig.kanayago(auto_start: true)
|
|
522
|
+
when :steep
|
|
523
|
+
ServerConfig.steep(auto_start: true)
|
|
481
524
|
else
|
|
482
|
-
raise ArgumentError, "Unknown server: #{name}. Use :solargraph, :ruby_lsp, :rubocop, or :
|
|
525
|
+
raise ArgumentError, "Unknown server: #{name}. Use :solargraph, :ruby_lsp, :rubocop, :kanayago, or :steep"
|
|
483
526
|
end
|
|
484
527
|
register_server(config)
|
|
485
528
|
end
|
|
486
529
|
|
|
487
530
|
private
|
|
488
531
|
|
|
532
|
+
# Picker helpers
|
|
533
|
+
|
|
534
|
+
def picker_active?(editor)
|
|
535
|
+
# Check if current buffer is the picker buffer
|
|
536
|
+
editor.buffer&.file_path == "[LSP Picker]"
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def handle_picker_select(ctx, new_tab:)
|
|
540
|
+
locations = ctx.editor.instance_variable_get(:@lsp_picker_locations)
|
|
541
|
+
return unless locations
|
|
542
|
+
|
|
543
|
+
# Get selected index from cursor position (line 3+ are the items, 0-indexed)
|
|
544
|
+
cursor_row = ctx.window.cursor_row
|
|
545
|
+
index = cursor_row - 2 # Skip header lines
|
|
546
|
+
return if index.negative? || index >= locations.length
|
|
547
|
+
|
|
548
|
+
location = locations[index]
|
|
549
|
+
close_picker(ctx.editor)
|
|
550
|
+
jump_to_location(ctx, location, new_tab: new_tab)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def close_picker(editor)
|
|
554
|
+
editor.instance_variable_set(:@lsp_picker_locations, nil)
|
|
555
|
+
editor.instance_variable_set(:@lsp_picker_type, nil)
|
|
556
|
+
# Close the picker window
|
|
557
|
+
editor.window_manager.close_current_window
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def jump_to_location(ctx, location, new_tab:)
|
|
561
|
+
file_path = location.file_path
|
|
562
|
+
unless file_path
|
|
563
|
+
ctx.set_message("Cannot open: #{location.uri}")
|
|
564
|
+
return
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
line = location.range.start.line
|
|
568
|
+
character = location.range.start.character
|
|
569
|
+
|
|
570
|
+
if new_tab
|
|
571
|
+
# Open in new tab (same as :tabnew command)
|
|
572
|
+
tab_manager = ctx.editor.tab_manager
|
|
573
|
+
new_tab_obj = tab_manager.add
|
|
574
|
+
new_buffer = Mui::Buffer.new
|
|
575
|
+
new_buffer.load(file_path)
|
|
576
|
+
new_buffer.undo_manager = Mui::UndoManager.new
|
|
577
|
+
new_tab_obj.window_manager.add_window(new_buffer)
|
|
578
|
+
else
|
|
579
|
+
# Open in current window
|
|
580
|
+
current_buffer = ctx.buffer
|
|
581
|
+
if current_buffer.file_path != file_path
|
|
582
|
+
new_buffer = Mui::Buffer.new
|
|
583
|
+
new_buffer.load(file_path)
|
|
584
|
+
ctx.editor.window.buffer = new_buffer
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
window = ctx.editor.window
|
|
589
|
+
window.cursor_row = line
|
|
590
|
+
window.cursor_col = character
|
|
591
|
+
window.ensure_cursor_visible
|
|
592
|
+
|
|
593
|
+
ctx.set_message("#{File.basename(file_path)}:#{line + 1}")
|
|
594
|
+
end
|
|
595
|
+
|
|
489
596
|
def create_manager(editor)
|
|
490
597
|
mgr = Manager.new(editor: editor)
|
|
491
598
|
# Register configured servers
|
|
@@ -526,8 +633,13 @@ module Mui
|
|
|
526
633
|
ServerConfig.rubocop_lsp(auto_start: true)
|
|
527
634
|
when :kanayago
|
|
528
635
|
ServerConfig.kanayago(auto_start: true)
|
|
636
|
+
when :typeprof
|
|
637
|
+
ServerConfig.typeprof(auto_start: true)
|
|
638
|
+
when :steep
|
|
639
|
+
ServerConfig.steep(auto_start: true)
|
|
529
640
|
else
|
|
530
|
-
raise ArgumentError,
|
|
641
|
+
raise ArgumentError,
|
|
642
|
+
"Unknown server: #{name}. Use :solargraph, :ruby_lsp, :rubocop, :kanayago, :typeprof, or :steep"
|
|
531
643
|
end
|
|
532
644
|
@server_configs << config
|
|
533
645
|
end
|
|
@@ -26,6 +26,8 @@ module Mui
|
|
|
26
26
|
case ext
|
|
27
27
|
when ".rb", ".rake", ".gemspec", ".ru"
|
|
28
28
|
"ruby"
|
|
29
|
+
when ".rbs"
|
|
30
|
+
"rbs"
|
|
29
31
|
when ".js"
|
|
30
32
|
"javascript"
|
|
31
33
|
when ".ts"
|
|
@@ -99,6 +101,26 @@ module Mui
|
|
|
99
101
|
)
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
def typeprof(auto_start: false)
|
|
105
|
+
new(
|
|
106
|
+
name: "typeprof",
|
|
107
|
+
command: "typeprof --lsp --stdio",
|
|
108
|
+
language_ids: ["ruby"],
|
|
109
|
+
file_patterns: ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
|
|
110
|
+
auto_start: auto_start
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def steep(auto_start: false)
|
|
115
|
+
new(
|
|
116
|
+
name: "steep",
|
|
117
|
+
command: "steep langserver",
|
|
118
|
+
language_ids: %w[ruby rbs],
|
|
119
|
+
file_patterns: ["**/*.rb", "**/*.rbs", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
|
|
120
|
+
auto_start: auto_start
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
102
124
|
def custom(name:, command:, language_ids:, file_patterns:, auto_start: true, sync_on_change: true)
|
|
103
125
|
new(
|
|
104
126
|
name: name,
|
data/lib/mui/lsp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mui-lsp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- S-H-GAMELINKS
|
|
@@ -43,8 +43,10 @@ files:
|
|
|
43
43
|
- lib/mui/lsp/handlers/completion.rb
|
|
44
44
|
- lib/mui/lsp/handlers/definition.rb
|
|
45
45
|
- lib/mui/lsp/handlers/diagnostics.rb
|
|
46
|
+
- lib/mui/lsp/handlers/formatting.rb
|
|
46
47
|
- lib/mui/lsp/handlers/hover.rb
|
|
47
48
|
- lib/mui/lsp/handlers/references.rb
|
|
49
|
+
- lib/mui/lsp/handlers/type_definition.rb
|
|
48
50
|
- lib/mui/lsp/highlighters/diagnostic_highlighter.rb
|
|
49
51
|
- lib/mui/lsp/json_rpc_io.rb
|
|
50
52
|
- lib/mui/lsp/manager.rb
|
|
@@ -83,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
85
|
- !ruby/object:Gem::Version
|
|
84
86
|
version: '0'
|
|
85
87
|
requirements: []
|
|
86
|
-
rubygems_version:
|
|
88
|
+
rubygems_version: 4.0.2
|
|
87
89
|
specification_version: 4
|
|
88
90
|
summary: LSP (Language Server Protocol) plugin for Mui editor
|
|
89
91
|
test_files: []
|