Skip to content

Commit 374c3bb

Browse files
committed
testing new lsp.ts
1 parent 9fc316a commit 374c3bb

File tree

1 file changed

+107
-131
lines changed
  • vscode/extension/src/lsp

1 file changed

+107
-131
lines changed

vscode/extension/src/lsp/lsp.ts

Lines changed: 107 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -20,177 +20,183 @@ import { CustomLSPMethods } from './custom'
2020
type SupportedMethodsState =
2121
| { type: 'not-fetched' }
2222
| { type: 'fetched'; methods: Set<string> }
23-
// TODO: This state is used when the `sqlmesh/supported_methods` endpoint is
24-
// not supported by the LSP server. This is in order to be backward compatible
25-
// with older versions of SQLMesh that do not support this endpoint. At some point
26-
// we should remove this state and always fetch the supported methods.
27-
| { type: 'endpoint-not-supported' }
23+
| { type: 'endpoint-not-supported' } // fallback for very old servers
2824

2925
let outputChannel: OutputChannel | undefined
3026

3127
export class LSPClient implements Disposable {
3228
private client: LanguageClient | undefined
33-
/**
34-
* State to track whether the supported methods have been fetched. These are used to determine if a method is supported
35-
* by the LSP server and return an error if not.
36-
*/
29+
30+
/** Caches which custom methods the server supports */
3731
private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' }
3832

3933
/**
40-
* Explicitly stopped remembers whether the LSP client has been explicitly stopped
41-
* by the user. This is used to prevent the client from being restarted unless the user
42-
* explicitly calls the `restart` method.
34+
* Remember whether the user explicitly stopped the client so that we do not
35+
* auto‑start again until they ask for it.
4336
*/
4437
private explicitlyStopped = false
4538

46-
constructor() {
47-
this.client = undefined
39+
// ───────────────────────────────────────────── helpers
40+
41+
/** True when a LanguageClient instance is alive. */
42+
private get isRunning(): boolean {
43+
return this.client !== undefined
4844
}
4945

50-
// TODO: This method is used to check if the LSP client has completion capability
51-
// in order to be backward compatible with older versions of SQLMesh that do not
52-
// support completion. At some point we should remove this method and always assume
53-
// that the LSP client has completion capability.
46+
// ───────────────────────────────────────────── public api
47+
48+
/**
49+
* Query whether the connected server advertises completion capability.
50+
* (Transient helper kept for backwards‑compat reasons.)
51+
*/
5452
public hasCompletionCapability(): boolean {
5553
if (!this.client) {
5654
traceError('LSP client is not initialized')
5755
return false
5856
}
59-
const capabilities = this.client.initializeResult?.capabilities
60-
const completion = capabilities?.completionProvider
61-
return completion !== undefined
57+
return (
58+
this.client.initializeResult?.capabilities?.completionProvider !==
59+
undefined
60+
)
6261
}
6362

63+
/** Start the Language Client unless it is already running. */
6464
public async start(
6565
overrideStoppedByUser = false,
6666
): Promise<Result<undefined, ErrorType>> {
67+
// 0. Respect the “user stopped” flag
6768
if (this.explicitlyStopped && !overrideStoppedByUser) {
6869
traceInfo(
6970
'LSP client has been explicitly stopped by user, not starting again.',
7071
)
7172
return ok(undefined)
7273
}
74+
75+
// 1. Guard against duplicate initialisation
76+
if (this.isRunning) {
77+
traceInfo('LSP client already running – start() is a no‑op.')
78+
return ok(undefined)
79+
}
80+
81+
// 2. Ensure we have an output channel
7382
if (!outputChannel) {
7483
outputChannel = window.createOutputChannel('sqlmesh-lsp')
7584
}
7685

86+
// 3. Resolve sqlmesh executable
7787
const sqlmesh = await sqlmeshLspExec()
7888
if (isErr(sqlmesh)) {
7989
traceError(
8090
`Failed to get sqlmesh_lsp_exec, ${JSON.stringify(sqlmesh.error)}`,
8191
)
8292
return sqlmesh
8393
}
84-
const workspaceFolders = getWorkspaceFolders()
85-
if (workspaceFolders.length === 0) {
86-
traceError(`No workspace folders found`)
87-
return err({
88-
type: 'generic',
89-
message: 'No workspace folders found',
90-
})
94+
95+
// 4. We need at least one workspace
96+
if (getWorkspaceFolders().length === 0) {
97+
const msg = 'No workspace folders found'
98+
traceError(msg)
99+
return err({ type: 'generic', message: msg })
91100
}
101+
92102
const workspacePath = sqlmesh.value.workspacePath
93103
const serverOptions: ServerOptions = {
94104
run: {
95105
command: sqlmesh.value.bin,
96106
transport: TransportKind.stdio,
97-
options: {
98-
cwd: workspacePath,
99-
env: sqlmesh.value.env,
100-
},
107+
options: { cwd: workspacePath, env: sqlmesh.value.env },
101108
args: sqlmesh.value.args,
102109
},
103110
debug: {
104111
command: sqlmesh.value.bin,
105112
transport: TransportKind.stdio,
106-
options: {
107-
cwd: workspacePath,
108-
env: sqlmesh.value.env,
109-
},
113+
options: { cwd: workspacePath, env: sqlmesh.value.env },
110114
args: sqlmesh.value.args,
111115
},
112116
}
113117
const clientOptions: LanguageClientOptions = {
114118
documentSelector: [
115-
{ scheme: 'file', pattern: `**/*.sql` },
116-
{
117-
scheme: 'file',
118-
pattern: '**/external_models.yaml',
119-
},
120-
{
121-
scheme: 'file',
122-
pattern: '**/external_models.yml',
123-
},
119+
{ scheme: 'file', pattern: '**/*.sql' },
120+
{ scheme: 'file', pattern: '**/external_models.yaml' },
121+
{ scheme: 'file', pattern: '**/external_models.yml' },
124122
],
125123
diagnosticCollectionName: 'sqlmesh',
126-
outputChannel: outputChannel,
124+
outputChannel,
127125
}
128126

129127
traceInfo(
130-
`Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`,
128+
`Starting SQLMesh LSP (cwd=${workspacePath})\n` +
129+
` serverOptions=${JSON.stringify(serverOptions)}\n` +
130+
` clientOptions=${JSON.stringify(clientOptions)}`,
131131
)
132+
132133
this.client = new LanguageClient(
133134
'sqlmesh-lsp',
134135
'SQLMesh Language Server',
135136
serverOptions,
136137
clientOptions,
137138
)
139+
this.explicitlyStopped = false // user wanted it running again
138140
await this.client.start()
139141
return ok(undefined)
140142
}
141143

144+
/** Restart = stop + start. */
142145
public async restart(
143-
overrideByUser = false,
146+
overrideStoppedByUser = false,
144147
): Promise<Result<undefined, ErrorType>> {
145-
await this.stop()
146-
return await this.start(overrideByUser)
148+
await this.stop() // this also disposes
149+
return this.start(overrideStoppedByUser)
147150
}
148151

152+
/**
153+
* Stop the client (if running) and clean up all VS Code resources so that a
154+
* future `start()` registers its commands without collisions.
155+
*/
149156
public async stop(stoppedByUser = false): Promise<void> {
150157
if (this.client) {
151-
traceInfo('Stopping SQLMesh Language Server')
152-
await this.client.stop()
158+
// Shut down the JSON‑RPC connection
159+
await this.client
160+
.stop()
161+
.catch(err => traceError(`Error while stopping LSP: ${err}`))
162+
163+
// **Important** – unregister commands, code lenses, etc.
164+
this.client.dispose()
165+
153166
this.client = undefined
154-
// Reset supported methods state when the client stops
155167
this.supportedMethodsState = { type: 'not-fetched' }
156-
traceInfo('SQLMesh Language Server stopped')
168+
traceInfo('SQLMesh LSP client disposed.')
157169
}
170+
158171
if (stoppedByUser) {
159172
this.explicitlyStopped = true
160173
traceInfo('SQLMesh LSP client stopped by user.')
161174
}
162175
}
163176

164-
public async dispose() {
177+
public async dispose(): Promise<void> {
165178
await this.stop()
166179
}
167180

181+
// ───────────────────────────────────────────── custom‑method helpers
182+
168183
private async fetchSupportedMethods(): Promise<void> {
169-
if (!this.client || this.supportedMethodsState.type !== 'not-fetched') {
184+
if (!this.client || this.supportedMethodsState.type !== 'not-fetched')
170185
return
171-
}
172-
try {
173-
const result = await this.internal_call_custom_method(
174-
'sqlmesh/supported_methods',
175-
{},
176-
)
177-
if (isErr(result)) {
178-
traceError(`Failed to fetch supported methods: ${result.error}`)
179-
this.supportedMethodsState = { type: 'endpoint-not-supported' }
180-
return
181-
}
182-
const methodNames = new Set(result.value.methods.map(m => m.name))
183-
this.supportedMethodsState = { type: 'fetched', methods: methodNames }
184-
traceInfo(
185-
`Fetched supported methods: ${Array.from(methodNames).join(', ')}`,
186-
)
187-
} catch {
188-
// If the supported_methods endpoint doesn't exist, mark it as not supported
186+
187+
const result = await this.internal_call_custom_method(
188+
'sqlmesh/supported_methods',
189+
{},
190+
)
191+
if (isErr(result)) {
192+
traceError(`Failed to fetch supported methods: ${result.error}`)
189193
this.supportedMethodsState = { type: 'endpoint-not-supported' }
190-
traceInfo(
191-
'Supported methods endpoint not available, proceeding without validation',
192-
)
194+
return
193195
}
196+
197+
const methodNames = new Set(result.value.methods.map(m => m.name))
198+
this.supportedMethodsState = { type: 'fetched', methods: methodNames }
199+
traceInfo(`Fetched supported methods: ${[...methodNames].join(', ')}`)
194200
}
195201

196202
public async call_custom_method<
@@ -210,79 +216,49 @@ export class LSPClient implements Disposable {
210216
>
211217
> {
212218
if (!this.client) {
213-
return err({
214-
type: 'generic',
215-
message: 'LSP client not ready.',
216-
})
219+
return err({ type: 'generic', message: 'LSP client not ready.' })
217220
}
221+
218222
await this.fetchSupportedMethods()
219223

220224
const supportedState = this.supportedMethodsState
221-
switch (supportedState.type) {
222-
case 'not-fetched':
223-
return err({
224-
type: 'invalid_state',
225-
message: 'Supported methods not fetched yet whereas they should.',
226-
})
227-
case 'fetched': {
228-
// If we have fetched the supported methods, we can check if the method is supported
229-
if (!supportedState.methods.has(method)) {
230-
return err({
231-
type: 'sqlmesh_outdated',
232-
message: `Method '${method}' is not supported by this LSP server.`,
233-
})
234-
}
235-
const response = await this.internal_call_custom_method(
236-
method,
237-
request as any,
238-
)
239-
if (isErr(response)) {
240-
return err({
241-
type: 'generic',
242-
message: response.error,
243-
})
244-
}
245-
return ok(response.value as Response)
246-
}
247-
case 'endpoint-not-supported': {
248-
const response = await this.internal_call_custom_method(
249-
method,
250-
request as any,
251-
)
252-
if (isErr(response)) {
253-
return err({
254-
type: 'generic',
255-
message: response.error,
256-
})
257-
}
258-
return ok(response.value as Response)
259-
}
225+
if (
226+
supportedState.type === 'fetched' &&
227+
!supportedState.methods.has(method)
228+
) {
229+
return err({
230+
type: 'sqlmesh_outdated',
231+
message: `Method '${method}' is not supported by this LSP server.`,
232+
})
233+
}
234+
235+
const response = await this.internal_call_custom_method(
236+
method,
237+
request as any,
238+
)
239+
if (isErr(response)) {
240+
return err({ type: 'generic', message: response.error })
260241
}
242+
return ok(response.value as Response)
261243
}
262244

263245
/**
264-
* Internal method to call a custom LSP method without checking if the method is supported. It is used for
265-
* the class whereas the `call_custom_method` checks if the method is supported.
246+
* Low‑level helper that sends a raw JSON‑RPC request without any feature checks.
266247
*/
267248
public async internal_call_custom_method<
268249
Method extends CustomLSPMethods['method'],
269250
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
270251
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
271252
>(method: Method, request: Request): Promise<Result<Response, string>> {
272-
if (!this.client) {
273-
return err('lsp client not ready')
274-
}
253+
if (!this.client) return err('lsp client not ready')
275254

276255
try {
277256
const result = await this.client.sendRequest<Response>(method, request)
278-
if (result.response_error) {
279-
return err(result.response_error)
280-
}
257+
if ((result as any).response_error)
258+
return err((result as any).response_error)
281259
return ok(result)
282260
} catch (error) {
283-
traceError(
284-
`lsp '${method}' request ${JSON.stringify(request)} failed: ${JSON.stringify(error)}`,
285-
)
261+
traceError(`LSP '${method}' request failed: ${JSON.stringify(error)}`)
286262
return err(JSON.stringify(error))
287263
}
288264
}

0 commit comments

Comments
 (0)