Class: Pikuri::Lsp::Anchor

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

Overview

A resolved query point: the server to ask, the document, and the exact position inside it. Every position-based operation needs one, and the model is never asked for a columnAnchor.resolve derives it.

resolution = Anchor.resolve(servers: servers, filesystem: filesystem,
                          symbol: 'resolve_for_read', file: 'read.rb', line: 165)
resolution.anchors.first.params
# => {textDocument: {uri: "file:///…/read.rb"}, position: {line: 164, character: 14}}

Why there is no character parameter

Not because the arithmetic is hard, but because nothing in the model's input contains a column: read prints line→text and grep prints file:line:text. A column could only be invented, and being wrong is not an error — one measured run had definition answering 0 locations from a keyword column, another had references seven columns off returning 80,609 locations instead of 2.

The two ways in, and only one is reliable

  • symbol + file + line → one line read, one string search, no index and no server involvement. Works on every server that will ever be wired.
  • symbol alone → workspace/symbol, which fails in opposite directions per server: ruby-lsp scores the query against the fully qualified name with a 0.7 Jaro-Winkler threshold, so a bare Confirmer scores 0.455 and is never returned, while jdtls indexes no methods at all. Best effort, reported honestly when it resolves nothing.

The keyword trap, which is why Anchor.locate runs at both ends

A symbol's range routinely begins at the keywordclass Foo starts at class — so feeding an index hit's range.start back in is the failure, not the shortcut: prepareTypeHierarchy at the keyword column returned zero items in 6 of 6 cases on ruby-lsp and one item every time at the symbol's own column. jdtls disagrees about the details in both directions and needs the same helper. So the column always comes from the line's text.

An anchor that resolves outside the root is dropped with its own note: every position-based operation would otherwise need a document outside what the servers index. What is lost is following a hop into a dependency, which hover and the jdt: content path still answer.

Defined Under Namespace

Classes: Resolution

Constant Summary collapse

MAX =

Anchors one call may fire. A name matching 14 declarations must not mean 14 fan-out queries, and a line mentioning the same identifier twice is queried at both columns on purpose (see locate).

5

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.locate(line_text, symbol) ⇒ Array<Integer>

1-based columns where symbol occurs in line_text.

locate('    names = names.trim()', 'names')   # => [5, 13]

Only occurrences bounded by non-identifier characters count, so greet does not match inside greeting. There is deliberately no any-occurrence fallback: a substring hit would anchor the query on a different name and the server would answer confidently about it, which is the one failure mode worth more than an empty answer. Columns are character columns — the encoding conversion happens later, once, in Position#to_wire.

Parameters:

  • line_text (String)
  • symbol (String)

Returns:

  • (Array<Integer>)

    empty when the symbol is not on the line, which is a clean error rather than a silently wrong answer.



99
100
101
102
103
104
105
106
107
# File 'lib/pikuri/lsp/anchor.rb', line 99

def self.locate(line_text, symbol)
  found = []
  offset = 0
  while (index = line_text.index(symbol, offset))
    found << index + 1 if bounded?(line_text, index, symbol.length)
    offset = index + 1
  end
  found
end

.no_server_message(path) ⇒ String

The degrade string, naming the extension rather than the servers: which servers are installed is machine state the model cannot act on, and the file type is what the call got wrong.

Parameters:

  • path (String, Pathname)

    the file nothing claimed.

Returns:

  • (String)


365
366
367
368
369
# File 'lib/pikuri/lsp/anchor.rb', line 365

def self.no_server_message(path)
  extension = File.extname(path.to_s)
  subject = extension.empty? ? File.basename(path.to_s) : "#{extension} files"
  "no language server is configured for #{subject}"
end

.resolve(servers:, filesystem:, symbol:, file: nil, line: nil, ready: ->(_client) {}) ⇒ Resolution

Resolve the anchors for one call.

Parameters:

  • servers (Servers)

    the live clients, for routing and fan-out.

  • filesystem (Pikuri::Workspace::Filesystem)

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

  • symbol (String)

    the name the model gave.

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

    narrows to one server and one document.

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

    1-based, as read and grep print it.

  • ready (Proc) (defaults to: ->(_client) {})

    called with a client before it is asked anything — the readiness gate, injected so this class needs no progress emitter.

Returns:

Raises:

  • (Refusal)

    when no anchor can be produced, with the reason: no server claims the file, the symbol is not on the named line, or the index has no entry for the name.

  • (Pikuri::Workspace::Filesystem::Error)

    when file is outside the workspace — the seam's own refusal, relayed by the tool.



134
135
136
137
138
# File 'lib/pikuri/lsp/anchor.rb', line 134

def self.resolve(servers:, filesystem:, symbol:, file: nil, line: nil, ready: ->(_client) {})
  return in_file(servers, filesystem, symbol, file, line, ready) if file

  in_index(servers, filesystem, symbol, ready)
end

Instance Method Details

#paramsHash

Returns the {textDocument:, position:} every position-based request carries, encoded in this server's negotiated positionEncoding.

Returns:

  • (Hash)

    the {textDocument:, position:} every position-based request carries, encoded in this server's negotiated positionEncoding.



72
73
74
75
# File 'lib/pikuri/lsp/anchor.rb', line 72

def params
  { textDocument: { uri: uri },
    position: position.to_wire(line_text: line_text, encoding: client.position_encoding) }
end

#to_sString

Returns "read.rb:165:15" — how a result block is labelled when one line held the symbol twice and the two columns disagreed.

Returns:

  • (String)

    "read.rb:165:15" — how a result block is labelled when one line held the symbol twice and the two columns disagreed.



79
80
81
# File 'lib/pikuri/lsp/anchor.rb', line 79

def to_s
  "#{File.basename(path)}:#{position}"
end

#uriString

Returns the anchor document's file: URI.

Returns:

  • (String)

    the anchor document's file: URI.



65
66
67
# File 'lib/pikuri/lsp/anchor.rb', line 65

def uri
  Uris.for_path(path)
end