Class: Pikuri::Lsp::Navigator

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/lsp/navigator.rb

Overview

One lsp call, end to end: route to a server, wait for its index, re-open the document, ask, render. LspTool holds the schema and the legs; this holds the behaviour, so a spec can drive it with no tool in sight.

navigator = Navigator.new(servers: servers, filesystem: filesystem,
                        on_progress: ->(progress) { ctx.emit_event(progress) })
navigator.navigate(operation: 'goToDefinition', symbol: 'resolve_for_read',
                 file: 'read.rb', line: 165)

The order of the five steps is the design

  1. Route on the file, or fan out when there is none — the model names no server, and a registry keyed by language would fail in exactly the mixed-language repo that makes a registry worth having.
  2. Gate on what that server advertised, per workspace and at dispatch. Extension routing without this is how a shipped client leaks Method not found: textDocument/implementation at the model.
  3. Wait for the index — the one place waiting beats relaying, because a mid-index server answers [] with a success code and there is nothing in [] to reason about. No timeout: a cold Java import measured 78 seconds, no number separates that from a hang, and progress events plus cancellable make the human the timeout.
  4. Re-open the document (ClientWrapper#open_document), and hand it back once every reply is in (#release_documents) — the pair is per call, because one call can ask several questions about one document.
  5. Ask and render, running the prepare hop and the per-level walk here so the model issues one call and never holds an opaque protocol item.

Errors are relayed, never worked around

Every server is broken in its own way and the ways do not generalize — one advertises a type hierarchy whose downward half is a # TODO, another indexes no methods, a third crashes on a notification the spec allows. So there is no per-server workaround and no known-stub list here: what the server said and what it was asked go back, and the model decides. That includes a reply pikuri cannot parse (#relaying) — the server's bug, and dying on it would be the one failure mode this stance exists to prevent.

Constant Summary collapse

MAX_PREPARED =

Items a prepare hop may produce before the rest are dropped with a note. prepare can legitimately answer several — Java overloads, one name declared in several classes — and the model has no more idea which is meant than this does, so each is queried and the results are labelled.

3

Instance Method Summary collapse

Constructor Details

#initialize(servers:, filesystem:, on_progress: nil, cancellable: nil) ⇒ Navigator

Returns a new instance of Navigator.

Parameters:

  • servers (Servers)

    the live clients.

  • filesystem (Pikuri::Workspace::Filesystem)

    path resolution, root confinement, the denylist pass and the seam every read routes through.

  • on_progress (Proc, nil) (defaults to: nil)

    called with a ServerProgress while a wait blocks — on this thread, which is what makes it safe to emit as an agent event.

  • cancellable (Pikuri::Agent::Control::Cancellable, nil) (defaults to: nil)

    polled by every wait and every request.



57
58
59
60
61
62
63
64
65
# File 'lib/pikuri/lsp/navigator.rb', line 57

def initialize(servers:, filesystem:, on_progress: nil, cancellable: nil)
  @servers = servers
  @filesystem = filesystem
  @on_progress = on_progress || ->(_progress) {}
  @cancellable = cancellable
  @ready = []
  @texts = {}
  @opened = []
end

Instance Method Details

Answer one call.

Parameters:

  • operation (String)

    an Operation name; Tool::Parameters has already refused anything outside the enum.

  • symbol (String)

    the name to ask about.

  • file (String, nil) (defaults to: nil)

    workspace-relative or absolute; the server selector, and the document an anchor is located in.

  • line (Integer, nil) (defaults to: nil)

    1-based, as read and grep print it.

Returns:

  • (String)

    the observation, including "Error: …" for everything the model can react to.

Raises:

  • (Pikuri::Agent::Control::Cancellable::Cancelled)

    on cancellation — deliberately not an observation.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/pikuri/lsp/navigator.rb', line 79

def navigate(operation:, symbol:, file: nil, line: nil)
  op = Operation[operation]
  raise ArgumentError, "unknown lsp operation #{operation.inspect}" if op.nil?

  renderer = Renderer.new(sources: Sources.new(filesystem: @filesystem))
  @ready = []
  @texts = {}
  @opened = []
  raise Refusal, 'no language server is configured' if @servers.empty?

  dispatch(op, symbol, file, line, renderer)
rescue Refusal, Pikuri::Workspace::Filesystem::Error, ClientWrapper::ServerDied => e
  "Error: #{e.message}"
rescue Connection::ServerError => e
  "Error: the language server refused #{operation}: #{e.message}"
ensure
  release_documents
end