@@ -20,177 +20,183 @@ import { CustomLSPMethods } from './custom'
2020type 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
2925let outputChannel : OutputChannel | undefined
3026
3127export 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