Skip to content

Commit e9dfb3f

Browse files
committed
feat(vscode): gracefully handle not supported apis
[ci skip]
1 parent 906ed7a commit e9dfb3f

File tree

8 files changed

+269
-5
lines changed

8 files changed

+269
-5
lines changed

sqlmesh/lsp/custom.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,30 @@ class AllModelsForRenderResponse(PydanticModel):
7272
"""
7373

7474
models: t.List[ModelForRendering]
75+
76+
77+
SUPPORTED_METHODS_FEATURE = "sqlmesh/supported_methods"
78+
79+
80+
class SupportedMethodsRequest(PydanticModel):
81+
"""
82+
Request to get all supported custom LSP methods.
83+
"""
84+
85+
pass
86+
87+
88+
class CustomMethod(PydanticModel):
89+
"""
90+
Information about a custom LSP method.
91+
"""
92+
93+
name: str
94+
95+
96+
class SupportedMethodsResponse(PydanticModel):
97+
"""
98+
Response containing all supported custom LSP methods.
99+
"""
100+
101+
methods: t.List[CustomMethod]

sqlmesh/lsp/main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929
ALL_MODELS_FEATURE,
3030
ALL_MODELS_FOR_RENDER_FEATURE,
3131
RENDER_MODEL_FEATURE,
32+
SUPPORTED_METHODS_FEATURE,
3233
AllModelsRequest,
3334
AllModelsResponse,
3435
AllModelsForRenderRequest,
3536
AllModelsForRenderResponse,
3637
RenderModelRequest,
3738
RenderModelResponse,
39+
SupportedMethodsRequest,
40+
SupportedMethodsResponse,
41+
CustomMethod,
3842
)
3943
from sqlmesh.lsp.hints import get_hints
4044
from sqlmesh.lsp.reference import get_references, get_cte_references
@@ -149,6 +153,30 @@ def all_models_for_render(
149153
models=self.lsp_context.list_of_models_for_rendering()
150154
)
151155

156+
@self.server.feature(SUPPORTED_METHODS_FEATURE)
157+
def supported_methods(
158+
ls: LanguageServer, params: SupportedMethodsRequest
159+
) -> SupportedMethodsResponse:
160+
"""Return all supported custom LSP methods."""
161+
methods = [
162+
CustomMethod(
163+
name=ALL_MODELS_FEATURE,
164+
),
165+
CustomMethod(
166+
name=RENDER_MODEL_FEATURE,
167+
),
168+
CustomMethod(
169+
name=ALL_MODELS_FOR_RENDER_FEATURE,
170+
),
171+
CustomMethod(
172+
name=API_FEATURE,
173+
),
174+
CustomMethod(
175+
name=SUPPORTED_METHODS_FEATURE,
176+
),
177+
]
178+
return SupportedMethodsResponse(methods=methods)
179+
152180
@self.server.feature(API_FEATURE)
153181
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
154182
ls.log_trace(f"API request: {request}")

vscode/extension/src/commands/format.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export const format =
3636
`Project format failed: ${out.error.message}`,
3737
)
3838
return
39+
case 'invalid_state':
40+
await vscode.window.showErrorMessage(
41+
`Project format failed due to invalid state: ${out.error.message}`,
42+
)
43+
return
44+
case 'sqlmesh_outdated':
45+
await vscode.window.showErrorMessage(
46+
`Project format failed due to outdated SQLMesh: ${out.error.message}`,
47+
)
48+
return
3949
}
4050
}
4151
vscode.window.showInformationMessage('Project formatted successfully')

vscode/extension/src/commands/renderModel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function renderModel(
7373

7474
if (isErr(allModelsResult)) {
7575
vscode.window.showErrorMessage(
76-
`Failed to get models: ${allModelsResult.error}`,
76+
`Failed to get models: ${allModelsResult.error.message}`,
7777
)
7878
return
7979
}
@@ -115,7 +115,9 @@ export function renderModel(
115115
})
116116

117117
if (isErr(result)) {
118-
vscode.window.showErrorMessage(`Failed to render model: ${result.error}`)
118+
vscode.window.showErrorMessage(
119+
`Failed to render model: ${result.error.message}`,
120+
)
119121
return
120122
}
121123

vscode/extension/src/extension.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ export async function activate(context: vscode.ExtensionContext) {
135135
`Failed to restart LSP: ${restartResult.error.message}`,
136136
)
137137
return
138+
case 'invalid_state':
139+
await vscode.window.showErrorMessage(
140+
`Failed to restart LSP due to invalid state: ${restartResult.error.message}`,
141+
)
142+
return
143+
case 'sqlmesh_outdated':
144+
await vscode.window.showErrorMessage(
145+
`Failed to restart LSP due to outdated SQLMesh: ${restartResult.error.message}`,
146+
)
147+
return
138148
}
139149
}
140150
context.subscriptions.push(lspClient)
@@ -173,6 +183,16 @@ export async function activate(context: vscode.ExtensionContext) {
173183
`Failed to start LSP: ${result.error.message}`,
174184
)
175185
break
186+
case 'invalid_state':
187+
await vscode.window.showErrorMessage(
188+
`Failed to start LSP due to invalid state: ${result.error.message}`,
189+
)
190+
break
191+
case 'sqlmesh_outdated':
192+
await vscode.window.showErrorMessage(
193+
`Failed to start LSP due to outdated SQLMesh: ${result.error.message}`,
194+
)
195+
break
176196
}
177197
} else {
178198
context.subscriptions.push(lspClient)

vscode/extension/src/lsp/custom.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type CustomLSPMethods =
3131
| AbstractAPICall
3232
| RenderModelMethod
3333
| AllModelsForRenderMethod
34+
| SupportedMethodsMethod
3435

3536
interface AllModelsRequest {
3637
textDocument: {
@@ -75,3 +76,20 @@ export interface ModelForRendering {
7576
description: string | null | undefined
7677
uri: string
7778
}
79+
80+
export interface SupportedMethodsMethod {
81+
method: 'sqlmesh/supported_methods'
82+
request: SupportedMethodsRequest
83+
response: SupportedMethodsResponse
84+
}
85+
86+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
87+
interface SupportedMethodsRequest {}
88+
89+
interface SupportedMethodsResponse {
90+
methods: CustomMethod[]
91+
}
92+
93+
interface CustomMethod {
94+
name: string
95+
}

vscode/extension/src/lsp/lsp.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,28 @@ import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh'
99
import { err, isErr, ok, Result } from '@bus/result'
1010
import { getWorkspaceFolders } from '../utilities/common/vscodeapi'
1111
import { traceError, traceInfo } from '../utilities/common/log'
12-
import { ErrorType } from '../utilities/errors'
12+
import {
13+
ErrorType,
14+
ErrorTypeGeneric,
15+
ErrorTypeInvalidState,
16+
ErrorTypeSQLMeshOutdated,
17+
} from '../utilities/errors'
1318
import { CustomLSPMethods } from './custom'
1419

20+
type SupportedMethodsState =
21+
| { type: 'not-fetched' }
22+
| { type: 'fetched'; methods: Set<string> }
23+
| { type: 'endpoint-not-supported' }
24+
1525
let outputChannel: OutputChannel | undefined
1626

1727
export class LSPClient implements Disposable {
1828
private client: LanguageClient | undefined
29+
/**
30+
* State to track whether the supported methods have been fetched. These are used to determine if a method is supported
31+
* by the LSP server and return an error if not.
32+
*/
33+
private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' }
1934

2035
constructor() {
2136
this.client = undefined
@@ -98,21 +113,123 @@ export class LSPClient implements Disposable {
98113
if (this.client) {
99114
await this.client.stop()
100115
this.client = undefined
116+
// Reset supported methods state when the client stops
117+
this.supportedMethodsState = { type: 'not-fetched' }
101118
}
102119
}
103120

104121
public async dispose() {
105122
await this.stop()
106123
}
107124

125+
private async fetchSupportedMethods(): Promise<void> {
126+
if (!this.client || this.supportedMethodsState.type !== 'not-fetched') {
127+
return
128+
}
129+
try {
130+
const result = await this.internal_call_custom_method(
131+
'sqlmesh/supported_methods',
132+
{},
133+
)
134+
if (isErr(result)) {
135+
traceError(`Failed to fetch supported methods: ${result.error}`)
136+
this.supportedMethodsState = { type: 'endpoint-not-supported' }
137+
return
138+
}
139+
const methodNames = new Set(result.value.methods.map(m => m.name))
140+
this.supportedMethodsState = { type: 'fetched', methods: methodNames }
141+
traceInfo(
142+
`Fetched supported methods: ${Array.from(methodNames).join(', ')}`,
143+
)
144+
} catch {
145+
// If the supported_methods endpoint doesn't exist, mark it as not supported
146+
this.supportedMethodsState = { type: 'endpoint-not-supported' }
147+
traceInfo(
148+
'Supported methods endpoint not available, proceeding without validation',
149+
)
150+
}
151+
}
152+
108153
public async call_custom_method<
154+
Method extends Exclude<
155+
CustomLSPMethods['method'],
156+
'sqlmesh/supported_methods'
157+
>,
158+
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
159+
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
160+
>(
161+
method: Method,
162+
request: Request,
163+
): Promise<
164+
Result<
165+
Response,
166+
ErrorTypeGeneric | ErrorTypeInvalidState | ErrorTypeSQLMeshOutdated
167+
>
168+
> {
169+
if (!this.client) {
170+
return err({
171+
type: 'generic',
172+
message: 'LSP client not ready.',
173+
})
174+
}
175+
await this.fetchSupportedMethods()
176+
177+
const supportedState = this.supportedMethodsState
178+
switch (supportedState.type) {
179+
case 'not-fetched':
180+
return err({
181+
type: 'invalid_state',
182+
message: 'Supported methods not fetched yet where as they should.',
183+
})
184+
case 'fetched': {
185+
// If we have fetched the supported methods, we can check if the method is supported
186+
if (!supportedState.methods.has(method)) {
187+
return err({
188+
type: 'sqlmesh_outdated',
189+
message: `Method '${method}' is not supported by this LSP server.`,
190+
})
191+
}
192+
const response = await this.internal_call_custom_method(
193+
method,
194+
request as any,
195+
)
196+
if (isErr(response)) {
197+
return err({
198+
type: 'generic',
199+
message: response.error,
200+
})
201+
}
202+
return ok(response.value as Response)
203+
}
204+
case 'endpoint-not-supported': {
205+
const response = await this.internal_call_custom_method(
206+
method,
207+
request as any,
208+
)
209+
if (isErr(response)) {
210+
return err({
211+
type: 'generic',
212+
message: response.error,
213+
})
214+
}
215+
return ok(response.value as Response)
216+
}
217+
}
218+
}
219+
220+
/**
221+
* Internal method to call a custom LSP method without checking if the method is supported. It is used for
222+
* the class where as the `call_custom_method` checks if the method is supported.
223+
*/
224+
public async internal_call_custom_method<
109225
Method extends CustomLSPMethods['method'],
110226
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
111227
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
112228
>(method: Method, request: Request): Promise<Result<Response, string>> {
113229
if (!this.client) {
114230
return err('lsp client not ready')
115231
}
232+
116233
try {
117234
const result = await this.client.sendRequest<Response>(method, request)
118235
return ok(result)

vscode/extension/src/utilities/errors.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,57 @@ import { traceInfo } from './common/log'
77
* Represents different types of errors that can occur in the application.
88
*/
99
export type ErrorType =
10-
| { type: 'generic'; message: string }
10+
| ErrorTypeGeneric
1111
| { type: 'not_signed_in' }
1212
| { type: 'sqlmesh_lsp_not_found' }
1313
// tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user
1414
// opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed.
1515
| { type: 'tcloud_bin_not_found' }
16-
// sqlmesh_lsp_dependencies_missing is used when the sqlmesh_lsp is found but the lsp extras are missing.
1716
| SqlmeshLspDependenciesMissingError
17+
| ErrorTypeInvalidState
18+
| ErrorTypeSQLMeshOutdated
1819

20+
/**
21+
* ErrorTypeSQLMeshOutdated is used when the SQLMesh version is outdated. The
22+
* message should explain the problem, but the suggestion to update SQLMesh is
23+
* handled at the place where the error is shown.
24+
*/
25+
export interface ErrorTypeSQLMeshOutdated {
26+
type: 'sqlmesh_outdated'
27+
/**
28+
* A message that describes the outdated SQLMesh version, it should not talk about
29+
* updating SQLMesh. This is done at the place where the error is handled.
30+
*/
31+
message: string
32+
}
33+
34+
/**
35+
* ErrorTypeInvalidState is used when the state of the application is invalid state.
36+
* They should never be thrown by the application unless there is a bug in the code.
37+
* The shown message should be generic and not contain any sensitive information but
38+
* asks the user to report the issue to the developers.
39+
*/
40+
export interface ErrorTypeInvalidState {
41+
type: 'invalid_state'
42+
/**
43+
* A message that describes the invalid state, it should not talk about reporting
44+
* the issue to the developers. This is done at the place where the error is
45+
* handled.
46+
*/
47+
message: string
48+
}
49+
50+
/**
51+
* ErrorTypeGeneric is a generic error type that can be used to represent any error with a message.
52+
*/
53+
export interface ErrorTypeGeneric {
54+
type: 'generic'
55+
message: string
56+
}
57+
58+
/**
59+
* sqlmesh_lsp_dependencies_missing is used when the sqlmesh_lsp is found, but the lsp extras are missing
60+
*/
1961
interface SqlmeshLspDependenciesMissingError {
2062
type: 'sqlmesh_lsp_dependencies_missing'
2163
is_missing_pygls: boolean

0 commit comments

Comments
 (0)