Skip to content

Commit cfac2fc

Browse files
committed
feat(vscode): add test recognition to vscode
[ci skip]
1 parent 2953942 commit cfac2fc

File tree

5 files changed

+139
-1
lines changed

5 files changed

+139
-1
lines changed

sqlmesh/lsp/context.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from sqlmesh.core.model.definition import SqlModel
77
from sqlmesh.core.linter.definition import AnnotatedRuleViolation
8-
from sqlmesh.lsp.custom import ModelForRendering
8+
from sqlmesh.lsp.custom import ModelForRendering, TestEntry
99
from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry
1010
from sqlmesh.lsp.uri import URI
1111
from lsprotocol import types
@@ -63,6 +63,18 @@ def __init__(self, context: Context) -> None:
6363
**audit_map,
6464
}
6565

66+
def list_workspace_tests(self) -> t.List[TestEntry]:
67+
"""List all tests in the workspace."""
68+
tests = self.context.load_model_tests()
69+
# TODO Probably want to get all the positions for the tests
70+
return [
71+
TestEntry(
72+
name=test.test_name,
73+
uri=URI.from_path(test.path).value,
74+
)
75+
for test in tests
76+
]
77+
6678
def render_model(self, uri: URI) -> t.List[RenderModelEntry]:
6779
"""Get rendered models for a file, using cache when available.
6880

sqlmesh/lsp/custom.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,27 @@ class FormatProjectResponse(CustomMethodResponseBaseClass):
143143
"""
144144

145145
pass
146+
147+
148+
LIST_WORKSPACE_TESTS_FEATURE = "sqlmesh/list_workspace_tests"
149+
150+
151+
class ListWorkspaceTestsRequest(CustomMethodRequestBaseClass):
152+
"""
153+
Request to list all tests in the current project.
154+
"""
155+
156+
pass
157+
158+
159+
class TestEntry(PydanticModel):
160+
"""
161+
An entry representing a test in the workspace.
162+
"""
163+
164+
name: str
165+
uri: str
166+
167+
168+
class ListWorkspaceTestsResponse(CustomMethodResponseBaseClass):
169+
tests: t.List[TestEntry]

sqlmesh/lsp/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
FormatProjectRequest,
4747
FormatProjectResponse,
4848
CustomMethod,
49+
LIST_WORKSPACE_TESTS_FEATURE,
50+
ListWorkspaceTestsRequest,
51+
ListWorkspaceTestsResponse,
4952
)
5053
from sqlmesh.lsp.errors import ContextFailedError, context_error_to_diagnostic
5154
from sqlmesh.lsp.hints import get_hints
@@ -126,11 +129,26 @@ def __init__(
126129
API_FEATURE: self._custom_api,
127130
SUPPORTED_METHODS_FEATURE: self._custom_supported_methods,
128131
FORMAT_PROJECT_FEATURE: self._custom_format_project,
132+
LIST_WORKSPACE_TESTS_FEATURE: self._list_workspace_tests,
129133
}
130134

131135
# Register LSP features (e.g., formatting, hover, etc.)
132136
self._register_features()
133137

138+
def _list_workspace_tests(
139+
self,
140+
ls: LanguageServer,
141+
params: ListWorkspaceTestsRequest,
142+
) -> ListWorkspaceTestsResponse:
143+
"""List all tests in the current workspace."""
144+
try:
145+
context = self._context_get_or_load()
146+
tests = context.list_workspace_tests()
147+
return ListWorkspaceTestsResponse(tests=tests)
148+
except Exception as e:
149+
ls.log_trace(f"Error listing workspace tests: {e}")
150+
return ListWorkspaceTestsResponse(tests=[])
151+
134152
# All the custom LSP methods are registered here and prefixed with _custom
135153
def _custom_all_models(self, ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse:
136154
uri = URI(params.textDocument.uri)

vscode/extension/src/lsp/custom.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,21 @@ interface FormatProjectResponse extends BaseResponse {}
111111
interface BaseResponse {
112112
response_error?: string
113113
}
114+
115+
export interface ListWorkspaceTests {
116+
method: 'sqlmesh/list_workspace_tests'
117+
request: ListWorkspaceTestsRequest
118+
response: ListWorkspaceTestsResponse
119+
}
120+
121+
type ListWorkspaceTestsRequest = object
122+
123+
interface TestEntry {
124+
name: string
125+
uri: string
126+
// TODO Probably want to add position at some point
127+
}
128+
129+
interface ListWorkspaceTestsResponse {
130+
tests: TestEntry[]
131+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as vscode from 'vscode'
2+
import { LSPClient } from '../lsp/lsp'
3+
4+
export const controller = vscode.tests.createTestController(
5+
'sqlmeshTests',
6+
'SQLMesh Tests',
7+
)
8+
9+
export const setupTestController = (lsp: LSPClient) => {
10+
// First, create the `resolveHandler`. This may initially be called with
11+
// "undefined" to ask for all tests in the workspace to be discovered, usually
12+
// when the user opens the Test Explorer for the first time.
13+
controller.resolveHandler = async test => {
14+
if (!test) {
15+
await discoverAllFilesInWorkspace()
16+
} else {
17+
await parseTestsInFileContents(test)
18+
}
19+
}
20+
21+
// When text documents are open, parse tests in them.
22+
vscode.workspace.onDidOpenTextDocument(parseTestsInDocument)
23+
// We could also listen to document changes to re-parse unsaved changes:
24+
vscode.workspace.onDidChangeTextDocument(e =>
25+
parseTestsInDocument(e.document),
26+
)
27+
28+
// In this function, we'll get the file TestItem if we've already found it,
29+
// otherwise we'll create it with `canResolveChildren = true` to indicate it
30+
// can be passed to the `controller.resolveHandler` to gets its children.
31+
function getOrCreateFile(uri: vscode.Uri) {
32+
const existing = controller.items.get(uri.toString())
33+
if (existing) {
34+
return existing
35+
}
36+
37+
const file = controller.createTestItem(
38+
uri.toString(),
39+
uri.path.split('/').pop()!,
40+
uri,
41+
)
42+
file.canResolveChildren = true
43+
return file
44+
}
45+
46+
function parseTestsInDocument(e: vscode.TextDocument) {
47+
if (e.uri.scheme === 'file' && e.uri.path.endsWith('.md')) {
48+
parseTestsInFileContents(getOrCreateFile(e.uri), e.getText())
49+
}
50+
}
51+
52+
async function parseTestsInFileContents(
53+
file: vscode.TestItem,
54+
contents?: string,
55+
) {
56+
// If a document is open, VS Code already knows its contents. If this is being
57+
// called from the resolveHandler when a document isn't open, we'll need to
58+
// read them from disk ourselves.
59+
if (contents === undefined) {
60+
const rawContent = await vscode.workspace.fs.readFile(file.uri)
61+
contents = new TextDecoder().decode(rawContent)
62+
}
63+
64+
// some custom logic to fill in test.children from the contents...
65+
}
66+
}

0 commit comments

Comments
 (0)