From 82c0aff87ab3a9d610662a732065e564ad304cfb Mon Sep 17 00:00:00 2001 From: bbrodriges Date: Sat, 16 Aug 2025 00:09:06 +0300 Subject: [PATCH] feat(lsp): allow to set custom env to lsp servers via config (#778) --- README.md | 7 ++++-- internal/config/config.go | 49 +++++++++++++++++++++++---------------- internal/lsp/client.go | 4 +++- schema.json | 9 ++++++- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 5f5f1ce80880de19ba60da0bf53a417c3d1d1cf7..48ba5fc38d32bfbf5e81a302ec79dabdcf2ce82c 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,10 @@ like you would. LSPs can be added manually like so: "$schema": "https://charm.land/crush.json", "lsp": { "go": { - "command": "gopls" + "command": "gopls", + "env": { + "GOTOOLCHAIN": "go1.24.5" + } }, "typescript": { "command": "typescript-language-server", @@ -433,7 +436,7 @@ accounts or OAuth workarounds, which may violate Anthropic and Microsoft’s Terms of Service. We’re committed to building sustainable, trusted integrations with model -providers. If you’re a provider interested in working with us, +providers. If you’re a provider interested in working with us, [reach out](mailto:vt100@charm.sh). ## Logging diff --git a/internal/config/config.go b/internal/config/config.go index d24f178a9023d9a627a9c271c712a4db75e8706c..7fef3b11d9b08f60d1ee9554bed27fd142536f7a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -117,11 +117,12 @@ type MCPConfig struct { } type LSPConfig struct { - Disabled bool `json:"enabled,omitempty" jsonschema:"description=Whether this LSP server is disabled,default=false"` - Command string `json:"command" jsonschema:"required,description=Command to execute for the LSP server,example=gopls"` - Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the LSP server command"` - Options any `json:"options,omitempty" jsonschema:"description=LSP server-specific configuration options"` - FileTypes []string `json:"filetypes,omitempty" jsonschema:"description=File types this LSP server handles,example=go,example=mod,example=rs,example=c,example=js,example=ts"` + Disabled bool `json:"enabled,omitempty" jsonschema:"description=Whether this LSP server is disabled,default=false"` + Command string `json:"command" jsonschema:"required,description=Command to execute for the LSP server,example=gopls"` + Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the LSP server command"` + Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set to the LSP server command"` + Options any `json:"options,omitempty" jsonschema:"description=LSP server-specific configuration options"` + FileTypes []string `json:"filetypes,omitempty" jsonschema:"description=File types this LSP server handles,example=go,example=mod,example=rs,example=c,example=js,example=ts"` } type TUIOptions struct { @@ -186,22 +187,12 @@ func (l LSPs) Sorted() []LSP { return sorted } -func (m MCPConfig) ResolvedEnv() []string { - resolver := NewShellVariableResolver(env.New()) - for e, v := range m.Env { - var err error - m.Env[e], err = resolver.ResolveValue(v) - if err != nil { - slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v) - continue - } - } +func (l LSPConfig) ResolvedEnv() []string { + return resolveEnvs(l.Env) +} - env := make([]string, 0, len(m.Env)) - for k, v := range m.Env { - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } - return env +func (m MCPConfig) ResolvedEnv() []string { + return resolveEnvs(m.Env) } func (m MCPConfig) ResolvedHeaders() map[string]string { @@ -509,3 +500,21 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error { _ = b.Body.Close() return nil } + +func resolveEnvs(envs map[string]string) []string { + resolver := NewShellVariableResolver(env.New()) + for e, v := range envs { + var err error + envs[e], err = resolver.ResolveValue(v) + if err != nil { + slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v) + continue + } + } + + res := make([]string, 0, len(envs)) + for k, v := range envs { + res = append(res, fmt.Sprintf("%s=%s", k, v)) + } + return res +} diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 32346fcb6ca95e8da292de81188c14cb3ce2f1f6..a6b9fcbb4caea4992fb2dbce6ddc6e75066c9da7 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "sync" "sync/atomic" @@ -66,8 +67,9 @@ type Client struct { // NewClient creates a new LSP client. func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Client, error) { cmd := exec.CommandContext(ctx, config.Command, config.Args...) + // Copy env - cmd.Env = os.Environ() + cmd.Env = slices.Concat(os.Environ(), config.ResolvedEnv()) stdin, err := cmd.StdinPipe() if err != nil { diff --git a/schema.json b/schema.json index 9f5bed67845fb3d56030fe233ffee1fbc0607478..4e7d8cdfcfd58d620be45be236c43ea2670a25ba 100644 --- a/schema.json +++ b/schema.json @@ -63,6 +63,13 @@ "type": "array", "description": "Arguments to pass to the LSP server command" }, + "env": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Environment variables to set to the LSP server command" + }, "options": { "description": "LSP server-specific configuration options" }, @@ -426,4 +433,4 @@ "type": "object" } } -} +} \ No newline at end of file