Acquire returns a ready Session for absPath: the pooled client for the file's project root and language server (spawned and initialized on first use), with the document opened or re-synchronized when its content changed since the last call. Callers must pass an absolute, cleaned path.
(ctx context.Context, absPath string)
| 101 | // use), with the document opened or re-synchronized when its content changed |
| 102 | // since the last call. Callers must pass an absolute, cleaned path. |
| 103 | func (p *Pool) Acquire(ctx context.Context, absPath string) (*Session, error) { |
| 104 | spec, ok := ServerForFile(absPath) |
| 105 | if !ok { |
| 106 | return nil, fmt.Errorf("no language server configured for %q files (supported: %s)", |
| 107 | filepath.Ext(absPath), strings.Join(SupportedExtensions(), " ")) |
| 108 | } |
| 109 | |
| 110 | data, err := os.ReadFile(absPath) //#nosec G304 -- agent-requested source file, same trust boundary as @read |
| 111 | if err != nil { |
| 112 | return nil, err |
| 113 | } |
| 114 | if len(data) > maxDocumentBytes { |
| 115 | return nil, fmt.Errorf("file too large for language-server analysis (%d bytes, limit %d)", len(data), maxDocumentBytes) |
| 116 | } |
| 117 | |
| 118 | root := findProjectRoot(absPath) |
| 119 | key := root + "\x00" + strings.Join(spec.Command, " ") |
| 120 | |
| 121 | p.mu.Lock() |
| 122 | defer p.mu.Unlock() |
| 123 | p.reapIdleLocked() |
| 124 | |
| 125 | entry, ok := p.entries[key] |
| 126 | if !ok { |
| 127 | // The server must OUTLIVE this call: exec.CommandContext ties the |
| 128 | // subprocess lifetime to its context, so spawning with the request |
| 129 | // ctx (or any ctx we cancel) kills the server the moment we return — |
| 130 | // the E2E symptom is an instant "broken pipe" on initialize. The pool |
| 131 | // owns the lifetime (idle reap + Close); only inherit values. |
| 132 | client, spawnErr := p.spawn(context.WithoutCancel(ctx), spec, p.logger) |
| 133 | if spawnErr != nil { |
| 134 | return nil, fmt.Errorf("start %s: %w (install it or override via %s)", spec.Command[0], spawnErr, spec.EnvKey) |
| 135 | } |
| 136 | rootURI := "file://" + root |
| 137 | if initErr := client.Initialize(rootURI); initErr != nil { |
| 138 | client.Shutdown() |
| 139 | return nil, fmt.Errorf("initialize %s: %w", spec.Command[0], initErr) |
| 140 | } |
| 141 | entry = &poolEntry{ |
| 142 | client: client, |
| 143 | docs: map[string]docState{}, |
| 144 | rootURI: rootURI, |
| 145 | spec: spec, |
| 146 | poolKey: key, |
| 147 | } |
| 148 | p.entries[key] = entry |
| 149 | p.logger.Info("lsp: language server started", |
| 150 | zap.String("server", spec.Command[0]), zap.String("root", root)) |
| 151 | } |
| 152 | entry.lastUse = p.now() |
| 153 | |
| 154 | uri := "file://" + absPath |
| 155 | if syncErr := entry.syncDocument(uri, spec.LanguageID, string(data)); syncErr != nil { |
| 156 | return nil, syncErr |
| 157 | } |
| 158 | return &Session{Client: entry.client, URI: uri, Root: root}, nil |
| 159 | } |
| 160 |