a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
771 lines
26 KiB
Go
771 lines
26 KiB
Go
package lsp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/semantic"
|
|
)
|
|
|
|
// ServerSpec describes one LSP server's invocation, the languages it
|
|
// covers, and the file extensions that route requests to it. Specs are
|
|
// the source of truth for both default-config registration (used by
|
|
// `gortex daemon` / `gortex mcp` / `gortex server`) and the runtime
|
|
// router (which dials a per-extension provider on first touch).
|
|
type ServerSpec struct {
|
|
// Name is the unique identifier used in config (e.g. "tsserver",
|
|
// "pyright", "rust-analyzer", "clangd", "jdtls").
|
|
Name string
|
|
// Command is the executable the daemon spawns (e.g. "rust-analyzer",
|
|
// "typescript-language-server", "pyright-langserver").
|
|
Command string
|
|
// Args is the argv tail. Most servers take stdio; servers that need
|
|
// `--stdio` or similar carry it here.
|
|
Args []string
|
|
// Languages is the set of internal language codes this server
|
|
// handles. Used by the manager's per-language priority routing.
|
|
Languages []string
|
|
// Extensions is the set of file extensions (with leading dot) that
|
|
// route to this server. Drives the runtime router.
|
|
Extensions []string
|
|
// LanguageIDs maps an extension to the LSP `languageId` to use in
|
|
// `textDocument/didOpen`. When an extension isn't listed, the first
|
|
// entry of Languages is used as a fallback.
|
|
LanguageIDs map[string]string
|
|
// Priority is the default priority used when no user override
|
|
// exists. Lower wins. Reserved 1-3 for user-tuned overrides; default
|
|
// LSP servers use 5.
|
|
Priority int
|
|
// Daemon hints whether the LSP server is intended to be kept alive
|
|
// across enrich passes. All known LSP servers benefit from this;
|
|
// the field is here for explicit future tuning.
|
|
Daemon bool
|
|
// MaxParallel caps concurrent LSP requests for this server. 10 is a
|
|
// safe starting point — gopls / rust-analyzer happily serve 10
|
|
// in-flight requests without backpressure.
|
|
MaxParallel int
|
|
// AlternativeCommands lists fallback executables to try when the
|
|
// primary `Command` is not on PATH. The first one that resolves
|
|
// wins. Useful for ecosystems with multiple bundled drivers
|
|
// (e.g. `tsserver` from `npm i -g typescript`, vs
|
|
// `typescript-language-server`).
|
|
AlternativeCommands []ServerAlt
|
|
// Env carries extra KEY=VALUE environment entries for the server
|
|
// subprocess, appended to the daemon's own environment. Empty for
|
|
// every built-in spec; populated only when a user overrides a
|
|
// server via .gortex.yaml (e.g. pinning JAVA_HOME for jdtls).
|
|
Env []string
|
|
// InitializationOptions carries server-specific initialization
|
|
// parameters sent in the "initialize" request. For jdtls this
|
|
// includes Maven/Gradle import settings so the server can resolve
|
|
// project dependencies instead of falling back to an "invisible
|
|
// project" with only JRE on the classpath. Nil/empty for servers
|
|
// that don't need it (gopls, rust-analyzer, etc.).
|
|
InitializationOptions json.RawMessage `json:"initializationOptions,omitempty"`
|
|
// Connect, when non-nil, switches this spec from spawn mode to
|
|
// passive-attach: instead of starting a subprocess via Command +
|
|
// Args, Gortex dials the configured network endpoint and uses
|
|
// the resulting net.Conn for JSON-RPC framing. The intended use
|
|
// is IDE-coexistence — share the LSP server the user's editor is
|
|
// already running, rather than spawning a duplicate 200-500 MB
|
|
// subprocess per language.
|
|
Connect *ConnectSpec
|
|
// UseWorkspaceBundleGemfile, when true, makes the router set
|
|
// BUNDLE_GEMFILE to the spawned workspace's own Gemfile (when one
|
|
// exists) in the server's environment. ruby-lsp runs a `bundle install`
|
|
// for a "composed bundle" on spawn unless BUNDLE_GEMFILE is already set;
|
|
// pointing it at the project Gemfile skips that install for enrichment.
|
|
UseWorkspaceBundleGemfile bool
|
|
// NeedsCompileDB marks a server that cannot produce full semantic
|
|
// signal without a compilation database in the workspace. clangd is
|
|
// the case: with no compile_commands.json every didOpen becomes a
|
|
// header-less fallback translation unit — a full preamble + AST
|
|
// rebuild per file — so the hover / hierarchy sweep pays a per-file
|
|
// rebuild for little return, and opening a header directly makes it a
|
|
// standalone TU. When this is set and no database is found, enrichment
|
|
// degrades to reference confirmation: it skips the sweep and header
|
|
// files and warns with the remediation.
|
|
NeedsCompileDB bool
|
|
// DefaultSweepMode overrides the global demand-gated default of the
|
|
// per-file hover / call-hierarchy sweep for this server. Empty keeps
|
|
// the "demand" default; "full" makes the server sweep every file
|
|
// unless the operator sets an explicit sweep mode (config or the
|
|
// GORTEX_LSP_SWEEP env), both of which still win. Set for a server
|
|
// whose net-new edges come mostly from call hierarchy the demand gate
|
|
// skips — rust-analyzer, whose ambiguous receivers resolve through
|
|
// std-library types the graph never indexes, so its recall lives in
|
|
// the full sweep rather than in cheaper static confirmation.
|
|
DefaultSweepMode string
|
|
// ProjectReady, when non-nil, reports whether a workspace has the
|
|
// project setup this server needs to resolve anything at all — e.g.
|
|
// node_modules for tsserver, whose every cross-file / import lookup
|
|
// returns "no package metadata" without it. When it returns
|
|
// (false, remediation) the enrichment pass is SKIPPED entirely (not
|
|
// merely degraded like NeedsCompileDB): such a server spends minutes
|
|
// indexing and hovering to produce zero semantic signal, and the
|
|
// tree-sitter floor still covers those files. This is the general form
|
|
// of NeedsCompileDB for servers whose failure is a missing dependency
|
|
// tree rather than a missing compilation database.
|
|
ProjectReady func(root string) (ready bool, remediation string)
|
|
}
|
|
|
|
// ConnectSpec carries the transport coordinates for passive LSP attach
|
|
// — Gortex dials an already-running LSP server (typically the one the
|
|
// user's IDE has started) instead of spawning its own. When the dial
|
|
// fails and FallbackSpawn is false (the default), Gortex refuses the
|
|
// language for that workspace rather than silently spinning up a second
|
|
// instance behind the user's back.
|
|
type ConnectSpec struct {
|
|
// Network is the dial network — "tcp" or "unix".
|
|
Network string
|
|
// Address is the dial address — host:port for tcp, socket path
|
|
// for unix.
|
|
Address string
|
|
// FallbackSpawn, when true, lets ensureClient fall back to
|
|
// spawning a subprocess (using the spec's Command + Args) if the
|
|
// dial fails after backoff. Default false: if the IDE's server
|
|
// is unreachable, the language is unavailable rather than racing
|
|
// the IDE for a port.
|
|
FallbackSpawn bool
|
|
}
|
|
|
|
// Validate reports any obvious misconfiguration in a ConnectSpec.
|
|
// Returns nil for a usable spec; otherwise an error describing the
|
|
// missing field. A nil receiver is treated as "not configured" (no
|
|
// error — the spec just stays in spawn mode).
|
|
func (c *ConnectSpec) Validate() error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(c.Network) == "" && strings.TrimSpace(c.Address) == "" {
|
|
return fmt.Errorf("lsp.connect: network and address are required")
|
|
}
|
|
if strings.TrimSpace(c.Network) == "" {
|
|
return fmt.Errorf("lsp.connect: network is required (tcp or unix)")
|
|
}
|
|
if strings.TrimSpace(c.Address) == "" {
|
|
return fmt.Errorf("lsp.connect: address is required")
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(c.Network)) {
|
|
case "tcp", "tcp4", "tcp6", "unix":
|
|
default:
|
|
return fmt.Errorf("lsp.connect: unsupported network %q (want tcp or unix)", c.Network)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServerAlt is one fallback command form for a server.
|
|
type ServerAlt struct {
|
|
Command string
|
|
Args []string
|
|
// InitializationOptions overrides the spec-level InitializationOptions
|
|
// when this alternative wins on PATH. The spec-level blob is keyed to
|
|
// the primary command, so an alternative from a different vendor
|
|
// (e.g. intelephense standing in for phpactor) can carry its own
|
|
// options. Empty means "inherit the spec's InitializationOptions".
|
|
InitializationOptions json.RawMessage `json:"initializationOptions,omitempty"`
|
|
// InitOptionsFunc, when non-nil, is consulted at initialize time with
|
|
// the resolved workspace root so an alternative can root a per-repo
|
|
// cache path inside Gortex's cache home — a path only known at resolve
|
|
// time, not as a baked literal. It wins over InitializationOptions when
|
|
// it returns a non-empty result.
|
|
InitOptionsFunc func(repoRoot string) json.RawMessage `json:"-"`
|
|
}
|
|
|
|
// Servers is the canonical list of LSP servers Gortex knows how to
|
|
// route. Order is meaningful only for the seed config — the runtime
|
|
// router uses the per-extension lookup table.
|
|
//
|
|
// Adding a new server: add the entry here and add its file extensions
|
|
// to extToSpec at init time. The default config picks it up
|
|
// automatically; users override priority / command via .gortex.yaml.
|
|
// JdtlsTrustBuildEnv, when set to a truthy value ("1" / "true"), opts jdtls
|
|
// into build-backed resolution (Maven/Gradle import + autobuild) for the
|
|
// indexed repository. OFF by default: import and autobuild RUN the indexed
|
|
// project's own build — Gradle build scripts execute during import, autobuild
|
|
// compiles the sources — which is code execution from the indexed repo. Only
|
|
// enable it for repositories you trust.
|
|
const JdtlsTrustBuildEnv = "GORTEX_LSP_JDTLS_TRUST_BUILD"
|
|
|
|
// jdtlsSafeInitOptions keeps jdtls in a no-build mode: JRE-only classpath, with
|
|
// Maven/Gradle import and autobuild DISABLED. Resolution is more limited (jdtls
|
|
// falls back to an "invisible project"), but indexing an untrusted Java repo
|
|
// never executes its build. The default; opt into the build variant via
|
|
// JdtlsTrustBuildEnv.
|
|
const jdtlsSafeInitOptions = `{
|
|
"settings": {
|
|
"java": {
|
|
"import": {
|
|
"maven": {"enabled": false},
|
|
"gradle": {"enabled": false}
|
|
},
|
|
"autobuild": {"enabled": false}
|
|
}
|
|
},
|
|
"bundles": [],
|
|
"workspaceFolders": "auto"
|
|
}`
|
|
|
|
// jdtlsBuildInitOptions enables Maven/Gradle import + autobuild so jdtls
|
|
// resolves real project dependencies. WARNING: import and autobuild execute the
|
|
// indexed project's build (arbitrary code for Gradle). Sent only when the
|
|
// operator has explicitly trusted the indexed repo via JdtlsTrustBuildEnv.
|
|
const jdtlsBuildInitOptions = `{
|
|
"settings": {
|
|
"java": {
|
|
"import": {
|
|
"maven": {"enabled": true},
|
|
"gradle": {"enabled": true}
|
|
},
|
|
"autobuild": {"enabled": true}
|
|
}
|
|
},
|
|
"bundles": [],
|
|
"workspaceFolders": "auto"
|
|
}`
|
|
|
|
// effectiveInitializationOptions returns the InitializationOptions to send for
|
|
// spec. jdtls is upgraded from its safe (no-build) defaults to build-backed
|
|
// options ONLY when the operator opted in via JdtlsTrustBuildEnv; every other
|
|
// server gets its spec options unchanged.
|
|
func effectiveInitializationOptions(spec *ServerSpec) json.RawMessage {
|
|
if spec == nil {
|
|
return nil
|
|
}
|
|
if spec.Command == "jdtls" && envTruthy(JdtlsTrustBuildEnv) {
|
|
return json.RawMessage(jdtlsBuildInitOptions)
|
|
}
|
|
return spec.InitializationOptions
|
|
}
|
|
|
|
// envTruthy reports whether environment variable name is set to "1" or "true".
|
|
func envTruthy(name string) bool {
|
|
v := strings.TrimSpace(os.Getenv(name))
|
|
return v == "1" || strings.EqualFold(v, "true")
|
|
}
|
|
|
|
var Servers = []ServerSpec{
|
|
{
|
|
Name: "gopls",
|
|
Command: "gopls",
|
|
Args: []string{"-remote=auto"},
|
|
Languages: []string{"go"},
|
|
Extensions: []string{".go"},
|
|
LanguageIDs: map[string]string{".go": "go"},
|
|
Priority: 3,
|
|
Daemon: true,
|
|
MaxParallel: 10,
|
|
},
|
|
{
|
|
Name: "typescript-language-server",
|
|
Command: "typescript-language-server",
|
|
Args: []string{"--stdio"},
|
|
Languages: []string{"typescript", "javascript"},
|
|
Extensions: []string{
|
|
".ts", ".tsx", ".mts", ".cts",
|
|
".js", ".jsx", ".mjs", ".cjs",
|
|
},
|
|
LanguageIDs: map[string]string{
|
|
".ts": "typescript",
|
|
".tsx": "typescriptreact",
|
|
".mts": "typescript",
|
|
".cts": "typescript",
|
|
".js": "javascript",
|
|
".jsx": "javascriptreact",
|
|
".mjs": "javascript",
|
|
".cjs": "javascript",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 10,
|
|
ProjectReady: tsProjectReady,
|
|
},
|
|
{
|
|
// tsgo is the native-Go TypeScript compiler (the
|
|
// @typescript/native-preview package). Its LSP mode is offered
|
|
// at a lower precedence than typescript-language-server so the
|
|
// default routing is unchanged for anyone who has both — a
|
|
// user opts in by raising its priority in .gortex.yaml.
|
|
Name: "tsgo",
|
|
Command: "tsgo",
|
|
Args: []string{"--lsp", "--stdio"},
|
|
Languages: []string{"typescript", "javascript"},
|
|
Extensions: []string{
|
|
".ts", ".tsx", ".mts", ".cts",
|
|
".js", ".jsx", ".mjs", ".cjs",
|
|
},
|
|
LanguageIDs: map[string]string{
|
|
".ts": "typescript",
|
|
".tsx": "typescriptreact",
|
|
".mts": "typescript",
|
|
".cts": "typescript",
|
|
".js": "javascript",
|
|
".jsx": "javascriptreact",
|
|
".mjs": "javascript",
|
|
".cjs": "javascript",
|
|
},
|
|
Priority: 6,
|
|
Daemon: true,
|
|
MaxParallel: 10,
|
|
ProjectReady: tsProjectReady,
|
|
},
|
|
{
|
|
Name: "pyright",
|
|
Command: "pyright-langserver",
|
|
Args: []string{"--stdio"},
|
|
Languages: []string{"python"},
|
|
Extensions: []string{".py", ".pyi"},
|
|
LanguageIDs: map[string]string{
|
|
".py": "python",
|
|
".pyi": "python",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 10,
|
|
AlternativeCommands: []ServerAlt{
|
|
// jedi-language-server / pylsp are common alternates.
|
|
{Command: "jedi-language-server"},
|
|
{Command: "pylsp"},
|
|
},
|
|
},
|
|
{
|
|
// pyrefly is Meta's Rust-based Python type checker (the Pyre
|
|
// successor). Offered at a lower precedence than pyright so the
|
|
// default routing is unchanged for anyone who has both — a user
|
|
// opts in by raising its priority in .gortex.yaml.
|
|
Name: "pyrefly",
|
|
Command: "pyrefly",
|
|
Args: []string{"lsp"},
|
|
Languages: []string{"python"},
|
|
Extensions: []string{".py", ".pyi"},
|
|
LanguageIDs: map[string]string{
|
|
".py": "python",
|
|
".pyi": "python",
|
|
},
|
|
Priority: 6,
|
|
Daemon: true,
|
|
MaxParallel: 10,
|
|
},
|
|
{
|
|
Name: "rust-analyzer",
|
|
Command: "rust-analyzer",
|
|
Languages: []string{"rust"},
|
|
Extensions: []string{".rs"},
|
|
LanguageIDs: map[string]string{
|
|
".rs": "rust",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 8,
|
|
// Rust method calls bind overwhelmingly to standard-library
|
|
// receiver types the graph never indexes, so static confirmation
|
|
// leaves rust-analyzer's net-new call-hierarchy edges on the table;
|
|
// its recall lives in the full sweep. Default to it (operator config
|
|
// / GORTEX_LSP_SWEEP still override).
|
|
DefaultSweepMode: sweepModeFull,
|
|
},
|
|
{
|
|
Name: "clangd",
|
|
Command: "clangd",
|
|
// `--background-index` keeps a project-wide symbol index hot in
|
|
// the daemon, which is essential for cross-file references and
|
|
// type-hierarchy precision in large C++ trees. `--header-insertion=never`
|
|
// avoids tactical edits when we only want graph signal.
|
|
// `--clang-tidy=false` disables lint matchers during enrichment:
|
|
// Gortex consumes semantic graph signal, not clang-tidy diagnostics,
|
|
// and a broad repo `.clang-tidy` can crash clangd mid-pass — which,
|
|
// with reconnect, becomes a crash→reconnect→reindex loop that pins the
|
|
// server at high CPU for the whole pass.
|
|
Args: []string{"--background-index", "--header-insertion=never", "--clang-tidy=false"},
|
|
Languages: []string{"c", "cpp", "objc", "objcpp"},
|
|
Extensions: []string{
|
|
".c", ".h",
|
|
".cc", ".cpp", ".cxx", ".c++",
|
|
".hh", ".hpp", ".hxx", ".h++",
|
|
".m", ".mm",
|
|
},
|
|
LanguageIDs: map[string]string{
|
|
".c": "c",
|
|
".h": "c",
|
|
".cc": "cpp",
|
|
".cpp": "cpp",
|
|
".cxx": "cpp",
|
|
".c++": "cpp",
|
|
".hh": "cpp",
|
|
".hpp": "cpp",
|
|
".hxx": "cpp",
|
|
".h++": "cpp",
|
|
".m": "objective-c",
|
|
".mm": "objective-cpp",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
// clangd's hover / call- and type-hierarchy signal needs a
|
|
// compilation database; without one it rebuilds a fallback AST
|
|
// per didOpen, so the enrichment pass degrades to reference
|
|
// confirmation instead of driving that churn.
|
|
NeedsCompileDB: true,
|
|
},
|
|
{
|
|
Name: "jdtls",
|
|
Command: "jdtls",
|
|
Languages: []string{"java"},
|
|
Extensions: []string{".java"},
|
|
LanguageIDs: map[string]string{
|
|
".java": "java",
|
|
},
|
|
Priority: 6, // jdtls is heavyweight; lower priority than scip-java.
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
// InitializationOptions for jdtls. SAFE BY DEFAULT (jdtlsSafeInitOptions):
|
|
// no Maven/Gradle import and no autobuild, so indexing an UNTRUSTED Java
|
|
// repo never runs its build. Build-backed resolution — which EXECUTES the
|
|
// repo's own build (Gradle scripts run on import; autobuild compiles) — is
|
|
// opt-in via GORTEX_LSP_JDTLS_TRUST_BUILD; see effectiveInitializationOptions.
|
|
InitializationOptions: json.RawMessage(jdtlsSafeInitOptions),
|
|
},
|
|
{
|
|
Name: "kotlin-language-server",
|
|
Command: "kotlin-language-server",
|
|
Languages: []string{"kotlin"},
|
|
Extensions: []string{".kt", ".kts"},
|
|
LanguageIDs: map[string]string{
|
|
".kt": "kotlin",
|
|
".kts": "kotlin",
|
|
},
|
|
Priority: 6,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
},
|
|
{
|
|
Name: "omnisharp",
|
|
Command: "omnisharp",
|
|
Args: []string{"-lsp"},
|
|
Languages: []string{"csharp"},
|
|
Extensions: []string{".cs"},
|
|
LanguageIDs: map[string]string{
|
|
".cs": "csharp",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
// csharp-ls is a Roslyn stdio LSP (`dotnet tool install csharp-ls`)
|
|
// that speaks plain LSP with no args and auto-discovers a .sln under
|
|
// the workspace root. It is far more commonly installed than
|
|
// OmniSharp on dev machines, so try it when omnisharp is not on PATH.
|
|
AlternativeCommands: []ServerAlt{
|
|
{Command: "csharp-ls"},
|
|
},
|
|
},
|
|
{
|
|
Name: "ruby-lsp",
|
|
Command: "ruby-lsp",
|
|
Languages: []string{"ruby"},
|
|
Extensions: []string{".rb", ".rake"},
|
|
LanguageIDs: map[string]string{
|
|
".rb": "ruby",
|
|
".rake": "ruby",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
UseWorkspaceBundleGemfile: true,
|
|
AlternativeCommands: []ServerAlt{
|
|
{Command: "solargraph", Args: []string{"stdio"}},
|
|
},
|
|
},
|
|
{
|
|
Name: "phpactor",
|
|
Command: "phpactor",
|
|
Args: []string{"language-server"},
|
|
Languages: []string{"php"},
|
|
Extensions: []string{".php"},
|
|
LanguageIDs: map[string]string{
|
|
".php": "php",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
AlternativeCommands: []ServerAlt{
|
|
// intelephense ships as a Node CLI. Pin its index cache under
|
|
// Gortex's cache home per repo so it doesn't write to its
|
|
// default global location outside the engine's isolation.
|
|
{Command: "intelephense", Args: []string{"--stdio"}, InitOptionsFunc: intelephenseInitOptions},
|
|
},
|
|
},
|
|
{
|
|
Name: "lua-language-server",
|
|
Command: "lua-language-server",
|
|
Languages: []string{"lua"},
|
|
Extensions: []string{".lua"},
|
|
LanguageIDs: map[string]string{
|
|
".lua": "lua",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "sourcekit-lsp",
|
|
Command: "sourcekit-lsp",
|
|
Languages: []string{"swift"},
|
|
Extensions: []string{".swift"},
|
|
LanguageIDs: map[string]string{
|
|
".swift": "swift",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 6,
|
|
},
|
|
{
|
|
Name: "haskell-language-server",
|
|
Command: "haskell-language-server-wrapper",
|
|
Args: []string{"--lsp"},
|
|
Languages: []string{"haskell"},
|
|
Extensions: []string{".hs", ".lhs"},
|
|
LanguageIDs: map[string]string{
|
|
".hs": "haskell",
|
|
".lhs": "haskell",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "elixir-ls",
|
|
Command: "elixir-ls",
|
|
Languages: []string{"elixir"},
|
|
Extensions: []string{".ex", ".exs"},
|
|
LanguageIDs: map[string]string{
|
|
".ex": "elixir",
|
|
".exs": "elixir",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "ocamllsp",
|
|
Command: "ocamllsp",
|
|
Languages: []string{"ocaml"},
|
|
Extensions: []string{".ml", ".mli"},
|
|
LanguageIDs: map[string]string{
|
|
".ml": "ocaml",
|
|
".mli": "ocaml",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "zls",
|
|
Command: "zls",
|
|
Languages: []string{"zig"},
|
|
Extensions: []string{".zig", ".zon"},
|
|
LanguageIDs: map[string]string{
|
|
".zig": "zig",
|
|
".zon": "zig",
|
|
},
|
|
Priority: 5,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "terraform-ls",
|
|
Command: "terraform-ls",
|
|
Args: []string{"serve"},
|
|
Languages: []string{"hcl"},
|
|
Extensions: []string{".tf", ".tfvars"},
|
|
LanguageIDs: map[string]string{
|
|
".tf": "terraform",
|
|
".tfvars": "terraform-vars",
|
|
},
|
|
Priority: 6,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "yaml-language-server",
|
|
Command: "yaml-language-server",
|
|
Args: []string{"--stdio"},
|
|
Languages: []string{"yaml"},
|
|
Extensions: []string{".yaml", ".yml"},
|
|
LanguageIDs: map[string]string{
|
|
".yaml": "yaml",
|
|
".yml": "yaml",
|
|
},
|
|
Priority: 7,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "vscode-json-language-server",
|
|
Command: "vscode-json-language-server",
|
|
Args: []string{"--stdio"},
|
|
Languages: []string{"json"},
|
|
Extensions: []string{".json", ".jsonc"},
|
|
LanguageIDs: map[string]string{
|
|
".json": "json",
|
|
".jsonc": "jsonc",
|
|
},
|
|
Priority: 7,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
{
|
|
Name: "bash-language-server",
|
|
Command: "bash-language-server",
|
|
Args: []string{"start"},
|
|
Languages: []string{"bash"},
|
|
Extensions: []string{".sh", ".bash"},
|
|
LanguageIDs: map[string]string{
|
|
".sh": "shellscript",
|
|
".bash": "shellscript",
|
|
},
|
|
Priority: 7,
|
|
Daemon: true,
|
|
MaxParallel: 4,
|
|
},
|
|
}
|
|
|
|
// extToSpec resolves a file extension (with leading dot, lower case) to
|
|
// its preferred LSP spec. Built once at init from `Servers`.
|
|
var extToSpec map[string]*ServerSpec
|
|
|
|
// nameToSpec resolves a server name (e.g. "rust-analyzer") to its spec.
|
|
var nameToSpec map[string]*ServerSpec
|
|
|
|
func init() {
|
|
extToSpec = make(map[string]*ServerSpec, 64)
|
|
nameToSpec = make(map[string]*ServerSpec, len(Servers))
|
|
for i := range Servers {
|
|
spec := &Servers[i]
|
|
nameToSpec[spec.Name] = spec
|
|
for _, ext := range spec.Extensions {
|
|
lower := strings.ToLower(ext)
|
|
// First registration wins; entries earlier in `Servers`
|
|
// take precedence when extensions overlap. (Used to keep
|
|
// gopls authoritative for `.go` even if a future entry
|
|
// claims it.)
|
|
if _, exists := extToSpec[lower]; !exists {
|
|
extToSpec[lower] = spec
|
|
}
|
|
}
|
|
}
|
|
|
|
// Contribute every known LSP spec to semantic.DefaultConfig()
|
|
// so the daemon picks them up automatically when the binary is
|
|
// on PATH. Each spec becomes a disabled-only-when-binary-missing
|
|
// ProviderConfig at the spec's default priority.
|
|
semantic.RegisterDefaultProviders(func() []semantic.ProviderConfig {
|
|
out := make([]semantic.ProviderConfig, 0, len(Servers))
|
|
for i := range Servers {
|
|
s := &Servers[i]
|
|
out = append(out, semantic.ProviderConfig{
|
|
Name: s.Name,
|
|
Command: s.Command,
|
|
Args: s.Args,
|
|
Languages: s.Languages,
|
|
Priority: s.Priority,
|
|
Enabled: true,
|
|
Daemon: s.Daemon,
|
|
MaxParallel: s.MaxParallel,
|
|
})
|
|
}
|
|
return out
|
|
})
|
|
}
|
|
|
|
// SpecForExtension returns the ServerSpec preferred for the given file
|
|
// extension (with or without leading dot). Returns nil when no spec
|
|
// covers the extension.
|
|
func SpecForExtension(ext string) *ServerSpec {
|
|
if ext == "" {
|
|
return nil
|
|
}
|
|
if !strings.HasPrefix(ext, ".") {
|
|
ext = "." + ext
|
|
}
|
|
return extToSpec[strings.ToLower(ext)]
|
|
}
|
|
|
|
// SpecForPath returns the ServerSpec covering the file's extension.
|
|
// Returns nil when no spec covers it.
|
|
func SpecForPath(path string) *ServerSpec {
|
|
return SpecForExtension(filepath.Ext(path))
|
|
}
|
|
|
|
// SpecByName returns the ServerSpec with the given name, or nil.
|
|
func SpecByName(name string) *ServerSpec {
|
|
return nameToSpec[name]
|
|
}
|
|
|
|
// SpecWithOverrides returns a copy of base with non-empty command /
|
|
// args / env overrides applied — the path by which .gortex.yaml tunes
|
|
// a heavyweight server (notably pinning a JRE + jdtls launcher args).
|
|
// base is never mutated, so the package-global Servers slice stays
|
|
// pristine across daemons and tests.
|
|
func SpecWithOverrides(base *ServerSpec, command string, args, env []string) *ServerSpec {
|
|
return SpecWithOverridesConnect(base, command, args, env, nil)
|
|
}
|
|
|
|
// SpecWithOverridesConnect is SpecWithOverrides plus an optional
|
|
// passive-attach block. When connect is non-nil and validates, the
|
|
// returned spec switches from spawn to dial — the Router constructs
|
|
// the LSP client by dialing the configured endpoint instead of running
|
|
// the spec's Command. A nil connect leaves spawn behaviour untouched.
|
|
func SpecWithOverridesConnect(base *ServerSpec, command string, args, env []string, connect *ConnectSpec) *ServerSpec {
|
|
if base == nil {
|
|
return nil
|
|
}
|
|
cp := *base
|
|
if command != "" {
|
|
cp.Command = command
|
|
}
|
|
if len(args) > 0 {
|
|
cp.Args = append([]string(nil), args...)
|
|
}
|
|
if len(env) > 0 {
|
|
cp.Env = append([]string(nil), env...)
|
|
}
|
|
if connect != nil {
|
|
// Shallow-copy so callers can't mutate our cached spec by
|
|
// retaining a handle to their input struct.
|
|
c := *connect
|
|
cp.Connect = &c
|
|
}
|
|
return &cp
|
|
}
|
|
|
|
// LanguageIDForPath returns the LSP `languageId` for the given file
|
|
// path using the preferred spec's mapping. Returns an empty string
|
|
// when no spec covers the extension.
|
|
func LanguageIDForPath(path string) string {
|
|
spec := SpecForPath(path)
|
|
if spec == nil {
|
|
return ""
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
if id, ok := spec.LanguageIDs[ext]; ok {
|
|
return id
|
|
}
|
|
if len(spec.Languages) > 0 {
|
|
return spec.Languages[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// AllSpecs returns the canonical list of known specs in registration
|
|
// order.
|
|
func AllSpecs() []*ServerSpec {
|
|
out := make([]*ServerSpec, 0, len(Servers))
|
|
for i := range Servers {
|
|
out = append(out, &Servers[i])
|
|
}
|
|
return out
|
|
}
|