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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac0edc6ad5bdcfb8d3c4705d7246ee8d49ea823e4693de1bf437a5ba4d60e264
4
+ data.tar.gz: 59e0fb00ab3c379304615e114fc94bc02e1693bb856d4c908f4f3b62b3b0d170
5
+ SHA512:
6
+ metadata.gz: df47053646164f89d037b3b8fcc06a08cf77243efe6ded43ce7571cee66a32d355b7d628466eb1f7879f55e18bfdc5fabb7386bcd73d101866c4263843998c94
7
+ data.tar.gz: 4231151ca03b149edf198725ee8e82b42a57db7bc99f0e8f8e85c77fb124ff981976b40180f1c4512716c97750d2b8ae3ecc6669c60a745c365ba7e8d0c10a11
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,73 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2025-12-11 11:19:34 UTC using RuboCop version 1.81.7.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
11
+ Lint/DuplicateBranch:
12
+ Exclude:
13
+ - 'lib/mui/lsp/highlighters/diagnostic_highlighter.rb'
14
+
15
+ # Offense count: 16
16
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
17
+ Metrics/AbcSize:
18
+ Max: 44
19
+
20
+ # Offense count: 6
21
+ # Configuration parameters: CountComments, CountAsOne.
22
+ Metrics/ClassLength:
23
+ Max: 361
24
+
25
+ # Offense count: 7
26
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
27
+ Metrics/CyclomaticComplexity:
28
+ Max: 26
29
+
30
+ # Offense count: 42
31
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
32
+ Metrics/MethodLength:
33
+ Max: 61
34
+
35
+ # Offense count: 4
36
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
37
+ Metrics/ParameterLists:
38
+ Max: 6
39
+
40
+ # Offense count: 6
41
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
42
+ Metrics/PerceivedComplexity:
43
+ Max: 14
44
+
45
+ # Offense count: 2
46
+ # Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
47
+ # AllowedMethods: call
48
+ # WaywardPredicates: nonzero?
49
+ Naming/PredicateMethod:
50
+ Exclude:
51
+ - 'lib/mui/lsp/manager.rb'
52
+ - 'lib/mui/lsp/request_manager.rb'
53
+
54
+ # Offense count: 1
55
+ # This cop supports safe autocorrection (--autocorrect).
56
+ # Configuration parameters: EnforcedStyle, AllowComments.
57
+ # SupportedStyles: empty, nil, both
58
+ Style/EmptyElse:
59
+ Exclude:
60
+ - 'lib/mui/lsp/client.rb'
61
+
62
+ # Offense count: 1
63
+ # This cop supports safe autocorrection (--autocorrect).
64
+ Style/IfUnlessModifier:
65
+ Exclude:
66
+ - 'lib/mui/lsp/json_rpc_io.rb'
67
+
68
+ # Offense count: 1
69
+ # This cop supports safe autocorrection (--autocorrect).
70
+ # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
71
+ # URISchemes: http, https
72
+ Layout/LineLength:
73
+ Max: 131
data/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-11
4
+
5
+ ### Added
6
+
7
+ - **Core Protocol Support**
8
+ - Position, Range, Location, Diagnostic data types
9
+ - JSON-RPC 2.0 communication layer with Content-Length header framing
10
+ - Request/response callback management
11
+
12
+ - **LSP Client**
13
+ - Async request/response handling
14
+ - Automatic server initialization
15
+ - Server capability negotiation
16
+
17
+ - **Text Document Synchronization**
18
+ - didOpen, didChange, didSave, didClose notifications
19
+ - Debounced change notifications (300ms default)
20
+ - Full document sync mode
21
+
22
+ - **Feature Handlers**
23
+ - Hover: Display documentation for symbol under cursor
24
+ - Definition: Jump to symbol definition
25
+ - References: Find all references to a symbol
26
+ - Completion: Code completion suggestions
27
+ - Diagnostics: Error/warning display with custom highlighter support
28
+
29
+ - **Pre-configured Servers**
30
+ - Solargraph (`solargraph stdio`)
31
+ - ruby-lsp (`ruby-lsp`)
32
+ - Kanayago (`kanayago`)
33
+ - RuboCop LSP mode (`rubocop --lsp`)
34
+
35
+ - **Plugin Integration**
36
+ - Commands: `:LspStart`, `:LspStop`, `:LspStatus`, `:LspHover`, `:LspDefinition`, `:LspReferences`, `:LspCompletion`, `:LspDiagnostics`
37
+ - Keymaps: `K` (hover), `gd` (definition), `gr` (references)
38
+ - Buffer hooks for automatic document synchronization
39
+ - Auto-start servers when opening matching files
40
+ - Diagnostic underline highlighting:
41
+ - `DiagnosticHighlighter` class for displaying LSP diagnostics with underlines
42
+ - Error (red), warning (yellow), information (blue), and hint (cyan) severity styles
43
+ - Automatic highlight refresh when diagnostics change
44
+ - Requires Mui's dynamic custom highlighter support
45
+ - Floating window hover display:
46
+ - Hover information now shown in floating popup window (requires Mui floating window support)
47
+ - Falls back to echo area display for older Mui versions
48
+ - `:LspDiagnosticShow` command to display diagnostic at cursor in floating window
49
+ - `\e` keymap to show diagnostic at cursor position
50
+ - `sync_on_change` option for ServerConfig:
51
+ - Controls whether `didChange` notifications are sent to the server
52
+ - Useful for running multiple LSP servers without conflicts
53
+ - ruby-lsp defaults to `sync_on_change: false`
54
+ - Multiple LSP server support:
55
+ - Notifications (didOpen, didChange, didSave, didClose) broadcast to all matching servers
56
+ - Each server can be configured independently with `sync_on_change` option
57
+ - Pending document queue for server startup:
58
+ - Documents opened before server is ready are queued
59
+ - Queued documents are sent automatically when server finishes initialization
60
+ - Fixes issue where diagnostics weren't shown for files opened during server startup
61
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 S-H-GAMELINKS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # mui-lsp
2
+
3
+ LSP (Language Server Protocol) plugin for [Mui](https://github.com/S-H-GAMELINKS/mui) editor.
4
+
5
+ ## Features
6
+
7
+ - **Hover**: Show documentation for symbol under cursor (`K` or `\h` or `:LspHover`)
8
+ - **Go to Definition**: Jump to symbol definition (`\d` or `:LspDefinition`)
9
+ - **Find References**: Show all references to symbol (`\r` or `:LspReferences`)
10
+ - **Completion**: Get code completion suggestions (`\c` or `:LspCompletion`)
11
+ - **Diagnostics**: Display errors and warnings from LSP server (`:LspDiagnostics`)
12
+
13
+ ## Supported Language Servers
14
+
15
+ Pre-configured servers for Ruby:
16
+
17
+ - **Solargraph** - Full-featured Ruby language server
18
+ - **ruby-lsp** - Shopify's Ruby language server
19
+ - **Kanayago** - Realtime Ruby Syntax Check server
20
+ - **RuboCop** (LSP mode) - Ruby linter with LSP support
21
+
22
+ Custom servers can be configured for other languages.
23
+
24
+ ## Installation
25
+
26
+ Add to your `.muirc`:
27
+
28
+ ```ruby
29
+ # ~/.muirc
30
+ Mui.use "mui-lsp"
31
+ ```
32
+
33
+ Or if installing from a local path:
34
+
35
+ ```ruby
36
+ # ~/.muirc
37
+ Mui.use "mui-lsp", path: "/path/to/mui-lsp"
38
+ ```
39
+
40
+ ## Configuration (Required)
41
+
42
+ **Important**: mui-lsp does not auto-detect LSP servers. You must explicitly configure which server(s) to use in your `.muirc`.
43
+
44
+ ### Quick Setup
45
+
46
+ Use the `Mui.lsp` DSL block to configure servers:
47
+
48
+ ```ruby
49
+ # ~/.muirc
50
+ Mui.use "mui-lsp"
51
+
52
+ Mui.lsp do
53
+ use :solargraph
54
+ end
55
+ ```
56
+
57
+ Available pre-configured servers:
58
+ - `:solargraph` - Solargraph (full-featured Ruby LSP)
59
+ - `:ruby_lsp` - ruby-lsp (Shopify's Ruby LSP)
60
+ - `:rubocop` - RuboCop in LSP mode
61
+ - `:kanayago` - Kanayago (Japanese Ruby LSP)
62
+
63
+ ### Custom Server Configuration
64
+
65
+ For other languages or custom setups, use the `server` method:
66
+
67
+ ```ruby
68
+ # ~/.muirc
69
+ Mui.use "mui-lsp"
70
+
71
+ Mui.lsp do
72
+ # TypeScript/JavaScript
73
+ server name: "typescript",
74
+ command: "typescript-language-server --stdio",
75
+ language_ids: ["typescript", "javascript"],
76
+ file_patterns: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
77
+
78
+ # Python (pyright)
79
+ server name: "pyright",
80
+ command: "pyright-langserver --stdio",
81
+ language_ids: ["python"],
82
+ file_patterns: ["**/*.py"]
83
+ end
84
+ ```
85
+
86
+ ### Multiple Servers
87
+
88
+ You can enable multiple servers for different file types:
89
+
90
+ ```ruby
91
+ # ~/.muirc
92
+ Mui.use "mui-lsp"
93
+
94
+ Mui.lsp do
95
+ # Ruby
96
+ use :solargraph
97
+
98
+ # TypeScript/JavaScript
99
+ server name: "typescript",
100
+ command: "typescript-language-server --stdio",
101
+ language_ids: ["typescript", "javascript"],
102
+ file_patterns: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
103
+ end
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ ### Starting LSP Server
109
+
110
+ LSP servers are automatically started when you open a file that matches their configured file patterns.
111
+
112
+ To manually start a server:
113
+
114
+ ```vim
115
+ :LspStart solargraph
116
+ :LspStart ruby-lsp
117
+ :LspStart rubocop
118
+ ```
119
+
120
+ ### Commands
121
+
122
+ | Command | Description |
123
+ |---------|-------------|
124
+ | `:LspStart <name>` | Start a specific LSP server |
125
+ | `:LspStop [name]` | Stop a server (all if no name given) |
126
+ | `:LspStatus` | Show running and registered servers |
127
+ | `:LspHover` | Show hover information |
128
+ | `:LspDefinition` | Go to definition |
129
+ | `:LspReferences` | Find all references |
130
+ | `:LspCompletion` | Show completion menu |
131
+ | `:LspDiagnostics` | Show diagnostics for current file |
132
+ | `:LspDiagnosticShow` | Show diagnostic at cursor in floating window |
133
+ | `:LspLog` | Show LSP server logs in a buffer |
134
+ | `:LspDebug` | Show debug information |
135
+ | `:LspOpen` | Manually notify LSP server about current file |
136
+
137
+ ### Keymaps
138
+
139
+ Leader key is `\` (backslash).
140
+
141
+ | Key | Mode | Description |
142
+ |-----|------|-------------|
143
+ | `K` | Normal | Show hover information (in floating window) |
144
+ | `\h` | Normal | Show hover information (alternative) |
145
+ | `\d` | Normal | Go to definition |
146
+ | `\r` | Normal | Find references |
147
+ | `\c` | Normal | Show completion |
148
+ | `\e` | Normal | Show diagnostic at cursor (in floating window) |
149
+ | `Esc` | Normal | Cancel leader pending state / Close floating window |
150
+
151
+ ## Architecture
152
+
153
+ ```
154
+ mui-lsp/
155
+ lib/mui/lsp/
156
+ protocol/ # LSP protocol definitions
157
+ position.rb # Position (line, character)
158
+ range.rb # Range (start, end positions)
159
+ location.rb # Location (URI, range)
160
+ diagnostic.rb # Diagnostic (error/warning/info)
161
+ handlers/ # Response handlers
162
+ base.rb # Base handler class
163
+ hover.rb # Hover response handler
164
+ definition.rb # Definition response handler
165
+ references.rb # References response handler
166
+ diagnostics.rb # Diagnostics notification handler
167
+ completion.rb # Completion response handler
168
+ json_rpc_io.rb # JSON-RPC 2.0 over stdio
169
+ request_manager.rb # Request ID and callback management
170
+ server_config.rb # Server configuration presets
171
+ client.rb # LSP client (manages server process)
172
+ text_document_sync.rb # Document synchronization
173
+ manager.rb # Multi-server manager
174
+ plugin.rb # Mui plugin integration
175
+ ```
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ cd mui-lsp
181
+ bundle install
182
+ bundle exec rake test
183
+ bundle exec rubocop
184
+ ```
185
+
186
+ ## License
187
+
188
+ MIT License
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Mui
6
+ module Lsp
7
+ # LSP Client - manages connection to a language server
8
+ class Client
9
+ attr_reader :server_capabilities, :initialized, :root_uri, :last_stderr
10
+
11
+ def initialize(command:, root_path:, on_notification: nil)
12
+ @command = command
13
+ @root_path = root_path
14
+ @root_uri = "file://#{root_path}"
15
+ @on_notification = on_notification
16
+ @initialized = false
17
+ @server_capabilities = {}
18
+ @process = nil
19
+ @io = nil
20
+ @request_manager = RequestManager.new
21
+ @reader_thread = nil
22
+ @running = false
23
+ end
24
+
25
+ def start
26
+ return if @running
27
+
28
+ # Split command into array for proper shell handling
29
+ cmd_parts = @command.is_a?(Array) ? @command : @command.split(/\s+/)
30
+ stdin, stdout, stderr, @process = Open3.popen3(*cmd_parts)
31
+ @io = JsonRpcIO.new(input: stdout, output: stdin)
32
+ @stderr = stderr
33
+ @running = true
34
+ @init_mutex = Mutex.new
35
+ @init_cv = ConditionVariable.new
36
+
37
+ start_reader_thread
38
+ start_stderr_thread
39
+ initialize_server
40
+
41
+ # Wait for initialization to complete (with timeout)
42
+ @init_mutex.synchronize do
43
+ unless @initialized
44
+ @init_cv.wait(@init_mutex, 10) # 10 second timeout
45
+ end
46
+ end
47
+ end
48
+
49
+ def stop
50
+ return unless @running
51
+
52
+ @running = false
53
+ shutdown
54
+ @reader_thread&.join(2)
55
+ begin
56
+ @process&.kill
57
+ rescue StandardError
58
+ nil
59
+ end
60
+ @request_manager.cancel_all
61
+ end
62
+
63
+ def running?
64
+ @running && @initialized
65
+ end
66
+
67
+ def started?
68
+ @running
69
+ end
70
+
71
+ def request(method, params = nil, &callback)
72
+ return nil unless @running
73
+
74
+ id = @request_manager.register(callback)
75
+ message = JsonRpcIO.build_request(id: id, method: method, params: params)
76
+ unless @io.write_message(message)
77
+ @running = false
78
+ @request_manager.cancel(id)
79
+ return nil
80
+ end
81
+ id
82
+ end
83
+
84
+ def notify(method, params = nil)
85
+ return unless @running
86
+
87
+ message = JsonRpcIO.build_notification(method: method, params: params)
88
+ return if @io.write_message(message)
89
+
90
+ @running = false
91
+ end
92
+
93
+ def hover(uri:, line:, character:, &callback)
94
+ request("textDocument/hover", {
95
+ textDocument: { uri: uri },
96
+ position: { line: line, character: character }
97
+ }, &callback)
98
+ end
99
+
100
+ def definition(uri:, line:, character:, &callback)
101
+ request("textDocument/definition", {
102
+ textDocument: { uri: uri },
103
+ position: { line: line, character: character }
104
+ }, &callback)
105
+ end
106
+
107
+ def references(uri:, line:, character:, include_declaration: true, &callback)
108
+ request("textDocument/references", {
109
+ textDocument: { uri: uri },
110
+ position: { line: line, character: character },
111
+ context: { includeDeclaration: include_declaration }
112
+ }, &callback)
113
+ end
114
+
115
+ def completion(uri:, line:, character:, &callback)
116
+ request("textDocument/completion", {
117
+ textDocument: { uri: uri },
118
+ position: { line: line, character: character }
119
+ }, &callback)
120
+ end
121
+
122
+ def did_open(uri:, language_id:, version:, text:)
123
+ notify("textDocument/didOpen", {
124
+ textDocument: {
125
+ uri: uri,
126
+ languageId: language_id,
127
+ version: version,
128
+ text: text
129
+ }
130
+ })
131
+ end
132
+
133
+ def did_change(uri:, version:, changes:)
134
+ notify("textDocument/didChange", {
135
+ textDocument: { uri: uri, version: version },
136
+ contentChanges: changes
137
+ })
138
+ end
139
+
140
+ def did_save(uri:, text: nil)
141
+ params = { textDocument: { uri: uri } }
142
+ params[:text] = text if text
143
+ notify("textDocument/didSave", params)
144
+ end
145
+
146
+ def did_close(uri:)
147
+ notify("textDocument/didClose", {
148
+ textDocument: { uri: uri }
149
+ })
150
+ end
151
+
152
+ private
153
+
154
+ def start_reader_thread
155
+ @reader_thread = Thread.new do
156
+ while @running
157
+ begin
158
+ message = @io.read_message
159
+ break if message.nil?
160
+
161
+ handle_message(message)
162
+ rescue IOError, Errno::EPIPE
163
+ # Pipe closed, server exited
164
+ break
165
+ rescue StandardError
166
+ # Log error but continue reading
167
+ break unless @running
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ def start_stderr_thread
174
+ @stderr_lines = []
175
+ @stderr_thread = Thread.new do
176
+ while @running
177
+ begin
178
+ line = @stderr.gets
179
+ break if line.nil?
180
+
181
+ # Store stderr for debugging
182
+ @stderr_lines << line.chomp
183
+ @last_stderr = @stderr_lines.last(10).join("\n")
184
+ rescue IOError
185
+ break
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def handle_message(message)
192
+ if JsonRpcIO.response?(message)
193
+ handle_response(message)
194
+ elsif JsonRpcIO.notification?(message)
195
+ handle_notification(message)
196
+ elsif JsonRpcIO.request?(message)
197
+ handle_server_request(message)
198
+ end
199
+ end
200
+
201
+ def handle_response(message)
202
+ id = message["id"]
203
+ result = message["result"]
204
+ error = message["error"]
205
+ @request_manager.handle_response(id, result: result, error: error)
206
+ end
207
+
208
+ def handle_notification(message)
209
+ @on_notification&.call(message["method"], message["params"])
210
+ end
211
+
212
+ def handle_server_request(message)
213
+ # Handle server-initiated requests (e.g., workspace/configuration)
214
+ id = message["id"]
215
+ method = message["method"]
216
+
217
+ response = JsonRpcIO.build_response(id: id, result: nil)
218
+ case method
219
+ when "window/workDoneProgress/create"
220
+ # Accept progress token creation
221
+ when "client/registerCapability"
222
+ # Accept capability registration
223
+ else
224
+ # Return empty result for unknown requests
225
+ end
226
+ @io.write_message(response)
227
+ end
228
+
229
+ def initialize_server
230
+ request("initialize", {
231
+ processId: Process.pid,
232
+ rootUri: @root_uri,
233
+ rootPath: @root_path,
234
+ capabilities: client_capabilities,
235
+ workspaceFolders: [
236
+ { uri: @root_uri, name: File.basename(@root_path) }
237
+ ]
238
+ }) do |result, error|
239
+ if error
240
+ @running = false
241
+ else
242
+ @server_capabilities = result&.dig("capabilities") || {}
243
+ notify("initialized", {})
244
+ @initialized = true
245
+ end
246
+ @init_mutex.synchronize { @init_cv.signal }
247
+ end
248
+ end
249
+
250
+ def shutdown
251
+ request("shutdown") do |_result, _error|
252
+ notify("exit")
253
+ end
254
+ rescue StandardError
255
+ # Ignore errors during shutdown
256
+ end
257
+
258
+ def client_capabilities
259
+ {
260
+ textDocument: {
261
+ hover: {
262
+ contentFormat: %w[plaintext markdown]
263
+ },
264
+ completion: {
265
+ completionItem: {
266
+ snippetSupport: false,
267
+ documentationFormat: %w[plaintext markdown]
268
+ }
269
+ },
270
+ definition: {
271
+ linkSupport: false
272
+ },
273
+ references: {},
274
+ publishDiagnostics: {
275
+ relatedInformation: true
276
+ },
277
+ synchronization: {
278
+ didSave: true,
279
+ willSave: false,
280
+ willSaveWaitUntil: false
281
+ }
282
+ },
283
+ workspace: {
284
+ workspaceFolders: true
285
+ }
286
+ }
287
+ end
288
+ end
289
+ end
290
+ end