Skip to content

Commit 4fde930

Browse files
committed
feat: add model autocomplete to vscode
[ci skip]
1 parent fc98f6f commit 4fde930

8 files changed

Lines changed: 125 additions & 39 deletions

File tree

examples/sushi/models/latest_order.sql

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ MODEL (
1111

1212
SELECT id, customer_id, start_ts, end_ts, event_date
1313
FROM sushi.orders
14-
ORDER BY event_date DESC LIMIT 1
15-
14+
ORDER BY event_date DESC LIMIT 1

sqlmesh/lsp/custom.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from lsprotocol import types
2+
import typing as t
3+
from sqlmesh.utils.pydantic import PydanticModel
4+
5+
ALL_MODELS_FEATURE = "sqlmesh/all_models"
6+
7+
8+
class AllModelsRequest(PydanticModel):
9+
"""
10+
Request to get all the models that are in the current project.
11+
"""
12+
13+
textDocument: types.TextDocumentIdentifier
14+
15+
16+
class AllModelsResponse(PydanticModel):
17+
"""
18+
Response to get all the models that are in the current project.
19+
"""
20+
21+
models: t.List[str]

sqlmesh/lsp/main.py

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sqlmesh.core.context import Context
1313
from sqlmesh.core.linter.definition import AnnotatedRuleViolation
1414
from sqlmesh.lsp.context import LSPContext
15+
from sqlmesh.lsp.custom import ALL_MODELS_FEATURE, AllModelsRequest, AllModelsResponse
1516
from sqlmesh.lsp.reference import get_model_definitions_for_a_path
1617

1718

@@ -38,6 +39,12 @@ def __init__(
3839
def _register_features(self) -> None:
3940
"""Register LSP features on the internal LanguageServer instance."""
4041

42+
@self.server.feature(ALL_MODELS_FEATURE)
43+
def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse:
44+
context = self._context_get_or_load(params.textDocument.uri)
45+
models = context.context.models
46+
return AllModelsResponse(models=[model.name for model in models.values()])
47+
4148
@self.server.feature(types.TEXT_DOCUMENT_DID_OPEN)
4249
def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None:
4350
context = self._context_get_or_load(params.text_document.uri)
@@ -130,43 +137,6 @@ def formatting(
130137
ls.show_message(f"Error formatting SQL: {e}", types.MessageType.Error)
131138
return []
132139

133-
@self.server.feature(types.TEXT_DOCUMENT_DEFINITION)
134-
def goto_definition(
135-
ls: LanguageServer, params: types.DefinitionParams
136-
) -> t.List[types.LocationLink]:
137-
"""Jump to an object's definition."""
138-
try:
139-
self._ensure_context_for_document(params.text_document.uri)
140-
document = ls.workspace.get_document(params.text_document.uri)
141-
if self.lsp_context is None:
142-
raise RuntimeError(f"No context found for document: {document.path}")
143-
144-
references = get_model_definitions_for_a_path(
145-
self.lsp_context, params.text_document.uri
146-
)
147-
if len(references) == 0:
148-
return []
149-
150-
return [
151-
types.LocationLink(
152-
target_uri=reference.uri,
153-
target_selection_range=types.Range(
154-
start=types.Position(line=0, character=0),
155-
end=types.Position(line=0, character=0),
156-
),
157-
target_range=types.Range(
158-
start=types.Position(line=0, character=0),
159-
end=types.Position(line=0, character=0),
160-
),
161-
origin_selection_range=reference.range,
162-
)
163-
for reference in references
164-
]
165-
166-
except Exception as e:
167-
ls.show_message(f"Error formatting SQL: {e}", types.MessageType.Error)
168-
return []
169-
170140
@self.server.feature(types.TEXT_DOCUMENT_DEFINITION)
171141
def goto_definition(
172142
ls: LanguageServer, params: types.DefinitionParams

vscode/extension/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
"command": "sqlmesh.signout",
5858
"title": "Sign out from Tobiko Cloud",
5959
"description": "SQLMesh"
60+
},
61+
{
62+
"command": "sqlmesh.allModels",
63+
"title": "Get all models",
64+
"description": "SQLMesh"
6065
}
6166
]
6267
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as vscode from 'vscode'
2+
import { LSPClient } from '../lsp/lsp'
3+
import { isErr } from '../utilities/functional/result'
4+
5+
export const selector: vscode.DocumentSelector = {
6+
pattern: '**/*.sql',
7+
}
8+
9+
export const completionProvider = (
10+
lsp: LSPClient,
11+
): vscode.CompletionItemProvider => {
12+
return {
13+
async provideCompletionItems(document) {
14+
const result = await lsp.call_custom_method('sqlmesh/all_models', {
15+
textDocument: {
16+
uri: document.uri.fsPath,
17+
},
18+
})
19+
const modelCompletions = isErr(result)
20+
? []
21+
: result.value.models.map((model): vscode.CompletionItem => {
22+
return new vscode.CompletionItem(
23+
model,
24+
vscode.CompletionItemKind.Module,
25+
)
26+
})
27+
return new vscode.CompletionList(modelCompletions)
28+
},
29+
}
30+
}

vscode/extension/src/extension.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
handleSqlmeshLspNotFoundError,
1919
handleSqlmeshLspDependenciesMissingError,
2020
} from './utilities/errors'
21+
import { completionProvider } from './completion/completion'
22+
import { selector } from './completion/completion'
2123

2224
let lspClient: LSPClient | undefined
2325

@@ -82,6 +84,13 @@ export async function activate(context: vscode.ExtensionContext) {
8284
context.subscriptions.push(lspClient)
8385
}
8486

87+
context.subscriptions.push(
88+
vscode.languages.registerCompletionItemProvider(
89+
selector,
90+
completionProvider(lspClient),
91+
),
92+
)
93+
8594
const restart = async () => {
8695
if (lspClient) {
8796
traceVerbose('Restarting LSP client')

vscode/extension/src/lsp/custom.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export interface AllModelsMethod {
2+
method: 'sqlmesh/all_models'
3+
request: AllModelsRequest
4+
response: AllModelsResponse
5+
}
6+
7+
export interface TestMethod {
8+
method: 'sqlmesh/test'
9+
request: TestRequest
10+
response: TestResponse
11+
}
12+
13+
// @eslint-disable-next-line @typescript-eslint/consistent-type-definition
14+
export type CustomLSPMethods = AllModelsMethod | TestMethod
15+
16+
interface AllModelsRequest {
17+
textDocument: {
18+
uri: string
19+
}
20+
}
21+
22+
interface AllModelsResponse {
23+
models: string[]
24+
}
25+
26+
interface TestRequest {
27+
foo: string
28+
}
29+
30+
interface TestResponse {
31+
bar: string
32+
}

vscode/extension/src/lsp/lsp.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { err, isErr, ok, Result } from '../utilities/functional/result'
1010
import { getWorkspaceFolders } from '../utilities/common/vscodeapi'
1111
import { traceError } from '../utilities/common/log'
1212
import { ErrorType } from '../utilities/errors'
13+
import { CustomLSPMethods } from './custom'
1314

1415
let outputChannel: OutputChannel | undefined
1516

@@ -98,4 +99,23 @@ export class LSPClient implements Disposable {
9899
public async dispose() {
99100
await this.stop()
100101
}
102+
103+
public async call_custom_method<
104+
Method extends CustomLSPMethods['method'],
105+
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
106+
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
107+
>(method: Method, request: Request): Promise<Result<Response, string>> {
108+
if (!this.client) {
109+
return err('lsp client not ready')
110+
}
111+
try {
112+
const result = await this.client.sendRequest<Response>(method, request)
113+
return ok(result)
114+
} catch (error) {
115+
traceError(
116+
`lsp '${method}' request ${JSON.stringify(request)} failed: ${JSON.stringify(error)}`,
117+
)
118+
return err(JSON.stringify(error))
119+
}
120+
}
101121
}

0 commit comments

Comments
 (0)