on main help sign in
feat(lsp): allow to set custom env to lsp servers via config (#778)
82c0affbbrodriges
README.md+5−2
@@ -182,7 +182,10 @@ like you would. LSPs can be added manually like so:
182 "$schema": "https://charm.land/crush.json",
182 "$schema": "https://charm.land/crush.json",
183 "lsp": {
183 "lsp": {
184 "go": {
184 "go": {
185− "command": "gopls"
185+ "command": "gopls",
186+ "env": {
187+ "GOTOOLCHAIN": "go1.24.5"
188+ }
186 },
189 },
187 "typescript": {
190 "typescript": {
188 "command": "typescript-language-server",
191 "command": "typescript-language-server",
@@ -433,7 +436,7 @@ accounts or OAuth workarounds, which may violate Anthropic and Microsoft’s
433Terms of Service.
436Terms of Service.
434
437
435We’re committed to building sustainable, trusted integrations with model
438We’re committed to building sustainable, trusted integrations with model
436−providers. If you’re a provider interested in working with us,
439+providers. If you’re a provider interested in working with us,
437[reach out](mailto:vt100@charm.sh).
440[reach out](mailto:vt100@charm.sh).
438
441
439## Logging
442## Logging
internal/config/config.go+29−20
@@ -117,11 +117,12 @@ type MCPConfig struct {
117}
117}
118
118
119type LSPConfig struct {
119type LSPConfig struct {
120− Disabled bool `json:"enabled,omitempty" jsonschema:"description=Whether this LSP server is disabled,default=false"`
120+ Disabled bool `json:"enabled,omitempty" jsonschema:"description=Whether this LSP server is disabled,default=false"`
121− Command string `json:"command" jsonschema:"required,description=Command to execute for the LSP server,example=gopls"`
121+ Command string `json:"command" jsonschema:"required,description=Command to execute for the LSP server,example=gopls"`
122− Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the LSP server command"`
122+ Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the LSP server command"`
123+ Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set to the LSP server command"`
123− Options any `json:"options,omitempty" jsonschema:"description=LSP server-specific configuration options"`
124+ Options any `json:"options,omitempty" jsonschema:"description=LSP server-specific configuration options"`
124− 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"`
125+ 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"`
125}
126}
126
127
127type TUIOptions struct {
128type TUIOptions struct {
@@ -186,22 +187,12 @@ func (l LSPs) Sorted() []LSP {
186 return sorted
187 return sorted
187}
188}
188
189
189−func (m MCPConfig) ResolvedEnv() []string {
190+func (l LSPConfig) ResolvedEnv() []string {
190− resolver := NewShellVariableResolver(env.New())
191+ return resolveEnvs(l.Env)
191− for e, v := range m.Env {
192− var err error
193− m.Env[e], err = resolver.ResolveValue(v)
194− if err != nil {
195− slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v)
196− continue
197− }
198− }
192+}
199
193
200− env := make([]string, 0, len(m.Env))
194+func (m MCPConfig) ResolvedEnv() []string {
201− for k, v := range m.Env {
202− env = append(env, fmt.Sprintf("%s=%s", k, v))
203− }
204− return env
195+ return resolveEnvs(m.Env)
205}
196}
206
197
207func (m MCPConfig) ResolvedHeaders() map[string]string {
198func (m MCPConfig) ResolvedHeaders() map[string]string {
@@ -509,3 +500,21 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
509 _ = b.Body.Close()
500 _ = b.Body.Close()
510 return nil
501 return nil
511}
502}
503+
504+func resolveEnvs(envs map[string]string) []string {
505+ resolver := NewShellVariableResolver(env.New())
506+ for e, v := range envs {
507+ var err error
508+ envs[e], err = resolver.ResolveValue(v)
509+ if err != nil {
510+ slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v)
511+ continue
512+ }
513+ }
514+
515+ res := make([]string, 0, len(envs))
516+ for k, v := range envs {
517+ res = append(res, fmt.Sprintf("%s=%s", k, v))
518+ }
519+ return res
520+}
internal/lsp/client.go+3−1
@@ -11,6 +11,7 @@ import (
11 "os"
11 "os"
12 "os/exec"
12 "os/exec"
13 "path/filepath"
13 "path/filepath"
14+ "slices"
14 "strings"
15 "strings"
15 "sync"
16 "sync"
16 "sync/atomic"
17 "sync/atomic"
@@ -66,8 +67,9 @@ type Client struct {
66// NewClient creates a new LSP client.
67// NewClient creates a new LSP client.
67func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Client, error) {
68func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Client, error) {
68 cmd := exec.CommandContext(ctx, config.Command, config.Args...)
69 cmd := exec.CommandContext(ctx, config.Command, config.Args...)
70+
69 // Copy env
71 // Copy env
70− cmd.Env = os.Environ()
72+ cmd.Env = slices.Concat(os.Environ(), config.ResolvedEnv())
71
73
72 stdin, err := cmd.StdinPipe()
74 stdin, err := cmd.StdinPipe()
73 if err != nil {
75 if err != nil {
schema.json+8−1
@@ -63,6 +63,13 @@
63 "type": "array",
63 "type": "array",
64 "description": "Arguments to pass to the LSP server command"
64 "description": "Arguments to pass to the LSP server command"
65 },
65 },
66+ "env": {
67+ "additionalProperties": {
68+ "type": "string"
69+ },
70+ "type": "object",
71+ "description": "Environment variables to set to the LSP server command"
72+ },
66 "options": {
73 "options": {
67 "description": "LSP server-specific configuration options"
74 "description": "LSP server-specific configuration options"
68 },
75 },
@@ -426,4 +433,4 @@
426 "type": "object"
433 "type": "object"
427 }
434 }
428 }
435 }
429−}
436+}