Class: Pikuri::Lsp::Registry

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

Overview

Pure configuration: which language servers exist, how to launch each, and which files each one claims. No I/O and no live state — a host writes this once and ClientWrapper spawns from it.

registry = Registry.new(entries: [
Registry::StdioEntry.new(id: 'ruby', command: %w[ruby-lsp],
                         files: %w[*.rb *.rake *.gemspec Rakefile Gemfile]),
Registry::StdioEntry.new(id: 'java', command: ['jdtls', '-data', data_dir],
                         files: %w[*.java],
                         env: { 'JAVA_HOME' => ENV.fetch('JAVA_HOME') },
                         init_options: { extendedClientCapabilities: {
                           classFileContentsSupport: true } })
])
registry.entry_for('lib/pikuri/agent.rb').id   # => "ruby"
registry.entry_for('Gemfile').id               # => "ruby"
registry.entry_for('README.md')                # => nil

A Ruby server is two facts, command and files; the Java one exercises every remaining field.

Defined Under Namespace

Classes: StdioEntry

Constant Summary collapse

LANGUAGE_IDS =

Extension → LSP language identifier, for entries that don't spell out a language_id:. Only the didOpen notification uses it, and a server given the wrong one indexes nothing, so an unmapped extension is a config error rather than a guess.

{
  '.rb' => 'ruby', '.rake' => 'ruby', '.gemspec' => 'ruby',
  '.java' => 'java', '.kt' => 'kotlin', '.kts' => 'kotlin',
  '.scala' => 'scala', '.groovy' => 'groovy', '.clj' => 'clojure',
  '.py' => 'python', '.js' => 'javascript', '.jsx' => 'javascriptreact',
  '.ts' => 'typescript', '.tsx' => 'typescriptreact',
  '.go' => 'go', '.rs' => 'rust', '.c' => 'c', '.h' => 'c',
  '.cpp' => 'cpp', '.cc' => 'cpp', '.hpp' => 'cpp', '.cs' => 'csharp',
  '.php' => 'php', '.swift' => 'swift', '.dart' => 'dart',
  '.ex' => 'elixir', '.exs' => 'elixir', '.erl' => 'erlang',
  '.hs' => 'haskell', '.lua' => 'lua', '.pl' => 'perl', '.r' => 'r',
  '.jl' => 'julia', '.sh' => 'shellscript', '.sql' => 'sql',
  '.html' => 'html', '.css' => 'css', '.scss' => 'scss',
  '.json' => 'json', '.yaml' => 'yaml', '.yml' => 'yaml',
  '.xml' => 'xml', '.md' => 'markdown', '.tex' => 'latex'
}.freeze
ENTRY_FIELDS =

The fields a StdioEntry accepts from a config file — everything but id, which is the key it is written under.

%i[command files language_id env init_options].freeze
EMPTY =

The no-servers default, so a host that configures nothing passes nothing.

new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries: []) ⇒ Registry

Returns a new instance of Registry.

Parameters:

  • entries (Array<StdioEntry>) (defaults to: [])

    zero or more servers. Order is the dispatch order.

Raises:

  • (ArgumentError)

    if two entries share an id — one would shadow the other's log lines and progress bars.



261
262
263
264
265
266
# File 'lib/pikuri/lsp/registry.rb', line 261

def initialize(entries: [])
  duplicate = entries.map(&:id).tally.find { |_, count| count > 1 }
  raise ArgumentError, "duplicate server id #{duplicate.first.inspect}" if duplicate

  @entries = entries.freeze
end

Instance Attribute Details

#entriesArray<StdioEntry> (readonly)

Returns the entries, in declaration order.

Returns:

  • (Array<StdioEntry>)

    the entries, in declaration order.



255
256
257
# File 'lib/pikuri/lsp/registry.rb', line 255

def entries
  @entries
end

Class Method Details

.derive_language_id(files, id) ⇒ String

LSP language id for a files: list.

Parameters:

  • files (Array<String>)

    basename globs.

  • id (String)

    the entry's id, for the error message.

Returns:

  • (String)

Raises:

  • (ArgumentError)

    when no glob carries a known extension.



243
244
245
246
247
248
249
250
251
252
# File 'lib/pikuri/lsp/registry.rb', line 243

def self.derive_language_id(files, id)
  files.each do |glob|
    extension = glob[/\.[A-Za-z0-9]+\z/]&.downcase
    language = extension && LANGUAGE_IDS[extension]
    return language if language
  end
  raise ArgumentError,
        "cannot derive language_id for #{id.inspect} from files: #{files.inspect}" \
        'pass language_id: explicitly'
end

.from_h(config) ⇒ Registry

Builds a registry out of a plain Hash, so a host can map its own config file onto one — which is where this belongs, because which language servers you have installed is machine state, like an API key. The demo scripts read the YAML they already keep keys in:

# ~/.pikuri-examples-config.yaml
lsp_servers:
ruby:
  command: [ruby-lsp]
  files: ["*.rb", "*.rake", "*.gemspec", Rakefile, Gemfile]
java:
  command: [jdtls, -data, /home/me/.cache/pikuri/jdtls/my-app]
  files: ["*.java"]
  env:
    JAVA_HOME: /usr/lib/jvm/java-21
  init_options:
    extendedClientCapabilities:
      classFileContentsSupport: true

Registry.from_h(YAML.safe_load_file(path)['lsp_servers'])

Nothing here knows what YAML is; a host reading TOML, JSON or its own settings object uses the same method. Keyed by server id rather than holding a list of {id: …} Hashes, which makes the duplicate #initialize refuses unwritable; insertion order is dispatch order.

Two things a config file cannot do, both visible above. It cannot compute, so jdtls's -data path is spelled out rather than derived from Paths.cache — a host that wants it computed builds the StdioEntry in Ruby, which is equally first-class. And it cannot be type-checked by the person writing it, so every field is validated here rather than coerced: a mistyped env is otherwise a jdtls that hard-fails below Java 21, and a mistyped init_options is every Java library symbol answering with silence.

Parameters:

  • config (Hash{String, Symbol => Hash}, nil)

    server id → that server's StdioEntry fields, with String or Symbol keys. nil — what a host reading an absent config key gets — yields an empty registry, so no caller needs a || {}.

Returns:

Raises:

  • (ArgumentError)

    on an unknown field, a field of the wrong type, or anything StdioEntry itself refuses (an unguessable language_id, an empty id).



157
158
159
# File 'lib/pikuri/lsp/registry.rb', line 157

def self.from_h(config)
  new(entries: (config || {}).map { |id, fields| entry_from_h(id.to_s, fields) })
end

Instance Method Details

#empty?Boolean

Returns whether no server is configured, in which case a host wires no lsp tool at all.

Returns:

  • (Boolean)

    whether no server is configured, in which case a host wires no lsp tool at all.



278
279
280
# File 'lib/pikuri/lsp/registry.rb', line 278

def empty?
  @entries.empty?
end

#entry_for(path) ⇒ StdioEntry?

The server that answers for path, or nil when none claims it.

Parameters:

  • path (String, Pathname)

Returns:



272
273
274
# File 'lib/pikuri/lsp/registry.rb', line 272

def entry_for(path)
  @entries.find { |entry| entry.claims?(path) }
end