Class: Pikuri::Lsp::Servers

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

Overview

Every language server this agent may use: one ClientWrapper per Registry entry, and the routing that picks which one answers for a file. Nothing is spawned until a call needs it — construction is pure, and each of the two routes starts what it needs:

servers = Servers.new(registry: registry, root: filesystem.project_root)
servers.client_for('lib/pikuri/agent.rb')   # the claiming server, spawned now
servers.client_for('README.md')             # => nil, and nothing spawned
servers.active                              # every project-relevant server, spawned
servers.close

Arm teardown straight after construction, not after the first call: a server that started before a later one failed is registered here precisely so an already-armed close still sweeps it.

Implementation details

Because a start is lazy, a misconfigured command is discovered by the call that needed it — a ClientWrapper::ServerDied naming the id and the argv, which Navigator relays as an "Error: …" observation. That is the same path a server dying after a successful spawn already took.

Sharing: P_one_agent, accepted for today — @clients / @active are unguarded, #close kills children another caller may be mid-request on, and a per-agent Agent::Control::Cancellable is threaded into every handshake. A shared server pool is buildable and simply isn't built, so N agents over one project means N sets of language servers.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry:, root:, cancellable: nil, spawn: nil) ⇒ Servers

Returns a new instance of Servers.

Parameters:

  • registry (Registry)

    which servers exist. Registry::EMPTY makes every method here a no-op answer, so a host with no config wires no tool and nothing else has to care.

  • root (String, Pathname)

    the workspace root: every child's cwd, the rootUri it indexes, and the tree #active_entries scans.

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

    threaded into each handshake.

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

    entry → a started ClientWrapper. The seam a spec replaces to drive fake servers over pipes; production passes nothing.



49
50
51
52
53
54
55
56
57
58
# File 'lib/pikuri/lsp/servers.rb', line 49

def initialize(registry:, root:, cancellable: nil, spawn: nil)
  @registry = registry
  @root = root
  @cancellable = cancellable
  @spawn = spawn || lambda { |entry|
    ClientWrapper.spawn(entry, root: @root, cancellable: @cancellable)
  }
  @clients = {}
  @active = nil
end

Instance Attribute Details

#registryRegistry (readonly)

Returns the configuration these clients run.

Returns:

  • (Registry)

    the configuration these clients run.



37
38
39
# File 'lib/pikuri/lsp/servers.rb', line 37

def registry
  @registry
end

Instance Method Details

#activeArray<ClientWrapper>

The fan-out set: every project-relevant server, each spawned here if it was not running.

Relevant rather than registered because a fan-out cannot choose between servers without starting them — Navigator gates on what each advertised in this workspace, and only a live handshake carries that.

Returns:

  • (Array<ClientWrapper>)

    in declaration order; empty when nothing is configured or nothing in the project matches.

Raises:



71
72
73
# File 'lib/pikuri/lsp/servers.rb', line 71

def active
  active_entries.map { |entry| client(entry) }
end

#active_entriesArray<Registry::StdioEntry>

The registered entries this project has files for — the relevance question answered without spawning anything.

Rescanned while the answer is incomplete, memoized once every registered entry is accounted for. A project gains its first .java file mid-session — the agent wrote it — and a scan memoized at the first call would leave jdtls out of every later fan-out, which the model reads as "no such symbol" rather than "nobody asked". A walk costs ~4 ms on this repo against a path that is about to block on a whole index, so paying it until the answer stops changing is free; once every entry is relevant nothing can invalidate it and the walk stops.

The first entry claiming a basename wins, exactly as Registry#entry_for decides, so an entry shadowed by an earlier one is never activated by files it would not be asked about.

Returns:



92
93
94
95
96
# File 'lib/pikuri/lsp/servers.rb', line 92

def active_entries
  return @active if @active && @active.size == @registry.entries.size

  @active = relevant_entries
end

#client_for(path) ⇒ ClientWrapper?

The server that answers for path, started if it was not running.

Cheap: the path is its own evidence of relevance, so this route never walks the project the way #active_entries must.

Parameters:

Returns:

Raises:



107
108
109
110
# File 'lib/pikuri/lsp/servers.rb', line 107

def client_for(path)
  entry = @registry.entry_for(path)
  entry && client(entry)
end

#closevoid

This method returns an undefined value.

Shut every started server down. Idempotent, and never raises — it runs from an on_close at exit, where one bad teardown must not take the rest of the sweep with it (ClientWrapper#close already swallows its own).



130
131
132
133
134
135
# File 'lib/pikuri/lsp/servers.rb', line 130

def close
  @clients.each_value(&:close)
  @clients.clear
  LOGGER.debug('closed')
  nil
end

#empty?Boolean

Returns whether there is nothing to ask — a registry question, not a liveness one: a configured server counts whether or not it has started, or this would answer "none" to the very first call.

Returns:

  • (Boolean)

    whether there is nothing to ask — a registry question, not a liveness one: a configured server counts whether or not it has started, or this would answer "none" to the very first call.



121
122
123
# File 'lib/pikuri/lsp/servers.rb', line 121

def empty?
  @registry.empty?
end

#started_idsArray<String>

Returns ids of the servers actually running, in start order — nothing at all until a call needs one.

Returns:

  • (Array<String>)

    ids of the servers actually running, in start order — nothing at all until a call needs one.



114
115
116
# File 'lib/pikuri/lsp/servers.rb', line 114

def started_ids
  @clients.keys
end