Class: Mbeditor::RubyLspClient
- Inherits:
-
Object
- Object
- Mbeditor::RubyLspClient
- Defined in:
- lib/mbeditor/ruby_lsp_client.rb
Overview
Manages a persistent ruby-lsp process per workspace and speaks the Language Server Protocol to it over stdio (Content-Length framing).
Lives in lib/ (required from lib/mbeditor.rb, NOT autoloaded) so Zeitwerk code reloads in the host's dev environment cannot wipe the registry and orphan the child process.
Usage:
client = RubyLspClient.for(workspace_root)
client.request_with_document("textDocument/definition", abs_path, content,
position: { line: 0, character: 4 })
Raises NotReadyError / TimeoutError for callers to fall back to the legacy grep/Ripper services.
Defined Under Namespace
Classes: Error, NotReadyError, TimeoutError
Constant Summary collapse
- INIT_TIMEOUT =
seconds for the initialize handshake
15- SHUTDOWN_GRACE =
seconds before pgroup KILL
2- MAX_RESTARTS =
3- RESTART_WINDOW =
seconds
300- RESTART_BACKOFFS =
min seconds between crash and retry
[1, 5, 25].freeze
- QUEUE_POP_SUPPORTS_TIMEOUT =
Queue#pop only accepts a timeout: keyword from Ruby 3.2 onwards, and the gem supports 3.0. Returns nil on timeout either way.
RUBY_VERSION >= "3.2"
Instance Attribute Summary collapse
-
#state ⇒ Object
readonly
Returns the value of attribute state.
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(root) ⇒ RubyLspClient
constructor
A new instance of RubyLspClient.
- #pop_with_timeout(queue, timeout) ⇒ Object
- #ready? ⇒ Boolean
- #request(method, params, timeout: nil) ⇒ Object
-
#request_with_document(method, path, content, params = {}, timeout: nil) ⇒ Object
Syncs the document (didOpen / full-text didChange) and issues a request against it under one mutex, so concurrent Puma threads can't interleave a positional request with a stale document.
- #stop ⇒ Object
Constructor Details
#initialize(root) ⇒ RubyLspClient
Returns a new instance of RubyLspClient.
68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 68 def initialize(root) @root = root @state_mutex = Mutex.new # lifecycle transitions @write_mutex = Mutex.new # stdin framing @pending_mutex = Mutex.new # id => Queue map + id allocation @doc_mutex = Mutex.new # document sync ordered with positional requests @pending = {} @docs = {} @next_id = 0 @state = :stopped # :stopped | :ready | :crashed | :failed @crash_times = [] end |
Instance Attribute Details
#state ⇒ Object (readonly)
Returns the value of attribute state.
81 82 83 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 81 def state @state end |
Class Method Details
.for(workspace_root) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 38 def for(workspace_root) root = workspace_root.to_s REGISTRY_MUTEX.synchronize do @registry ||= {} unless @at_exit_registered at_exit { stop_all } @at_exit_registered = true end @registry[root] ||= new(root) end end |
.reset! ⇒ Object
Tests only.
62 63 64 65 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 62 def reset! stop_all REGISTRY_MUTEX.synchronize { @registry = {} } end |
.stop_all ⇒ Object
50 51 52 53 54 55 56 57 58 59 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 50 def stop_all clients = REGISTRY_MUTEX.synchronize { (@registry || {}).values.dup } clients.each do |c| begin c.stop rescue StandardError nil end end end |
Instance Method Details
#pop_with_timeout(queue, timeout) ⇒ Object
127 128 129 130 131 132 133 134 135 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 127 def pop_with_timeout(queue, timeout) return queue.pop(timeout: timeout) if QUEUE_POP_SUPPORTS_TIMEOUT begin Timeout.timeout(timeout) { queue.pop } rescue Timeout::Error nil end end |
#ready? ⇒ Boolean
83 84 85 86 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 83 def ready? ensure_started @state == :ready end |
#request(method, params, timeout: nil) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 103 def request(method, params, timeout: nil) timeout ||= (Mbeditor.configuration.ruby_lsp_timeout || 3).to_f queue = Queue.new id = @pending_mutex.synchronize do @next_id += 1 @pending[@next_id] = queue @next_id end ({ jsonrpc: "2.0", id: id, method: method, params: params }) msg = pop_with_timeout(queue, timeout) raise TimeoutError, "#{method} timed out after #{timeout}s" if msg.nil? raise Error, msg["error"]["message"].to_s if msg["error"] msg["result"] ensure @pending_mutex.synchronize { @pending.delete(id) } if id end |
#request_with_document(method, path, content, params = {}, timeout: nil) ⇒ Object
Syncs the document (didOpen / full-text didChange) and issues a request
against it under one mutex, so concurrent Puma threads can't interleave
a positional request with a stale document.
params is an explicit hash (not keywords) so it can't collide with the
timeout: keyword.
93 94 95 96 97 98 99 100 101 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 93 def request_with_document(method, path, content, params = {}, timeout: nil) raise NotReadyError, "ruby-lsp is not running" unless ready? uri = "file://#{path}" @doc_mutex.synchronize do sync_document(uri, content) request(method, params.merge(textDocument: { uri: uri }), timeout: timeout) end end |
#stop ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/mbeditor/ruby_lsp_client.rb', line 137 def stop @state_mutex.synchronize do next unless @wait_thr @stopping = true begin request("shutdown", nil, timeout: 1) rescue StandardError nil end begin ({ jsonrpc: "2.0", method: "exit" }) rescue StandardError nil end unless @wait_thr.join(SHUTDOWN_GRACE) begin Process.kill("-KILL", @wait_thr.pid) rescue StandardError nil end end cleanup_process @state = :stopped @stopping = false end end |