Skip to content

Commit 39b1aab

Browse files
committed
feat(vscode): allow configuring project path
1 parent 8028656 commit 39b1aab

5 files changed

Lines changed: 94 additions & 79 deletions

File tree

sqlmesh/lsp/main.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
6969
# Use user-provided instantiator to build the context
7070
created_context = self.context_class(paths=[folder_path])
7171
self.lsp_context = LSPContext(created_context)
72-
ls.show_message(
73-
f"Loaded SQLMesh context from {config_path}",
74-
types.MessageType.Info,
75-
)
72+
loaded_sqlmesh_message(ls, folder_path)
7673
return # Exit after successfully loading any config
7774
except Exception as e:
7875
ls.show_message(
@@ -94,6 +91,9 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons
9491
@self.server.feature(API_FEATURE)
9592
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
9693
ls.log_trace(f"API request: {request}")
94+
if self.lsp_context is None:
95+
current_path = Path.cwd()
96+
self._ensure_context_in_folder(current_path)
9797
if self.lsp_context is None:
9898
raise RuntimeError("No context found")
9999

@@ -291,6 +291,20 @@ def _context_get_or_load(self, document_uri: URI) -> LSPContext:
291291
raise RuntimeError("No context found")
292292
return self.lsp_context
293293

294+
def _ensure_context_in_folder(self, folder_uri: Path) -> None:
295+
if self.lsp_context is not None:
296+
return
297+
for ext in ("py", "yml", "yaml"):
298+
config_path = folder_uri / f"config.{ext}"
299+
if config_path.exists():
300+
try:
301+
created_context = self.context_class(paths=[folder_uri])
302+
self.lsp_context = LSPContext(created_context)
303+
loaded_sqlmesh_message(self.server, folder_uri)
304+
return
305+
except Exception as e:
306+
self.server.show_message(f"Error loading context: {e}", types.MessageType.Error)
307+
294308
def _ensure_context_for_document(
295309
self,
296310
document_uri: URI,
@@ -382,6 +396,13 @@ def start(self) -> None:
382396
self.server.start_io()
383397

384398

399+
def loaded_sqlmesh_message(ls: LanguageServer, folder: Path) -> None:
400+
ls.show_message(
401+
f"Loaded SQLMesh context from {folder}",
402+
types.MessageType.Info,
403+
)
404+
405+
385406
def main() -> None:
386407
# Example instantiator that just uses the same signature as your original `Context` usage.
387408
sqlmesh_server = SQLMeshLanguageServer(context_class=Context)

vscode/extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"sqlmesh.projectPath": {
3535
"type": "string",
3636
"default": "",
37-
"markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute or relative to the workspace root."
37+
"markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root."
3838
}
3939
}
4040
},

vscode/extension/src/lsp/lsp.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh'
99
import { err, isErr, ok, Result } from '@bus/result'
1010
import { getWorkspaceFolders } from '../utilities/common/vscodeapi'
11-
import { traceError } from '../utilities/common/log'
11+
import { traceError, traceInfo } from '../utilities/common/log'
1212
import { ErrorType } from '../utilities/errors'
1313
import { CustomLSPMethods } from './custom'
1414

@@ -43,9 +43,6 @@ export class LSPClient implements Disposable {
4343
message: 'Invalid number of workspace folders',
4444
})
4545
}
46-
47-
const folder = workspaceFolders[0]
48-
// Use the workspace path from sqlmesh config, which respects the projectPath setting
4946
const workspacePath = sqlmesh.value.workspacePath
5047
const serverOptions: ServerOptions = {
5148
run: {
@@ -67,11 +64,13 @@ export class LSPClient implements Disposable {
6764
}
6865
const clientOptions: LanguageClientOptions = {
6966
documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }],
70-
workspaceFolder: folder,
7167
diagnosticCollectionName: 'sqlmesh',
7268
outputChannel: outputChannel,
7369
}
7470

71+
traceInfo(
72+
`Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`,
73+
)
7574
this.client = new LanguageClient(
7675
'sqlmesh-lsp',
7776
'SQLMesh Language Server',

vscode/extension/src/utilities/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export interface SqlmeshConfiguration {
1616
export function getSqlmeshConfiguration(): SqlmeshConfiguration {
1717
const config = workspace.getConfiguration('sqlmesh')
1818
const projectPath = config.get<string>('projectPath', '')
19-
2019
return {
2120
projectPath,
2221
}

vscode/extension/tests/lineage.spec.ts

Lines changed: 64 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,110 +14,106 @@ const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'su
1414
* Helper function to launch VS Code and test lineage with given project path config
1515
*/
1616
async function testLineageWithProjectPath(
17-
workspaceDir: string,
18-
projectDir: string,
19-
projectPathConfig?: string
17+
window: Page,
2018
): Promise<void> {
21-
const ciArgs = process.env.CI ? [
22-
'--disable-gpu',
23-
'--headless',
24-
'--no-sandbox',
25-
'--disable-dev-shm-usage',
26-
'--window-position=-10000,0',
27-
] : [];
28-
29-
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-'));
30-
31-
try {
32-
// If projectPathConfig is provided, create .vscode/settings.json in the workspace
33-
if (projectPathConfig !== undefined) {
34-
const vscodeDir = path.join(workspaceDir, '.vscode');
35-
await fs.ensureDir(vscodeDir);
36-
const settings = {
37-
"sqlmesh.projectPath": projectPathConfig
38-
};
39-
await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { spaces: 2 });
40-
}
41-
42-
const args = [
43-
...ciArgs,
44-
`--extensionDevelopmentPath=${EXT_PATH}`,
45-
'--disable-workspace-trust',
46-
'--disable-telemetry',
47-
`--user-data-dir=${userDataDir}`,
48-
workspaceDir,
49-
];
50-
51-
const electronApp = await electron.launch({
52-
executablePath: VS_CODE_EXE,
53-
args,
54-
});
55-
56-
const window = await electronApp.firstWindow();
57-
await window.waitForLoadState('domcontentloaded');
58-
await window.waitForLoadState('networkidle');
59-
60-
// Wait a bit for the extension to fully initialize with the settings
61-
await window.waitForTimeout(2000);
62-
6319
// Trigger lineage command
6420
await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P');
6521
await window.keyboard.type('Lineage: Focus On View');
6622
await window.keyboard.press('Enter');
6723

6824
// Wait for "Loaded SQLmesh Context" text to appear
6925
const loadedContextText = window.locator('text=Loaded SQLMesh Context');
70-
await expect(loadedContextText.first()).toBeVisible({ timeout: 15000 });
26+
// TODO REVERT TIMINGS
27+
await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 });
28+
}
7129

30+
/**
31+
* Launch VS Code and return the window and a function to close the app.
32+
* @param workspaceDir The workspace directory to open.
33+
* @returns The window and a function to close the app.
34+
*/
35+
export const startVSCode = async (workspaceDir: string): Promise<{
36+
window: Page,
37+
close: () => Promise<void>,
38+
}> => {
39+
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-'));
40+
const ciArgs = process.env.CI ? [
41+
'--disable-gpu',
42+
'--headless',
43+
'--no-sandbox',
44+
'--disable-dev-shm-usage',
45+
'--window-position=-10000,0',
46+
] : [];
47+
const args = [
48+
...ciArgs,
49+
`--extensionDevelopmentPath=${EXT_PATH}`,
50+
'--disable-workspace-trust',
51+
'--disable-telemetry',
52+
`--user-data-dir=${userDataDir}`,
53+
workspaceDir,
54+
];
55+
const electronApp = await electron.launch({
56+
executablePath: VS_CODE_EXE,
57+
args,
58+
});
59+
const window = await electronApp.firstWindow();
60+
await window.waitForLoadState('domcontentloaded');
61+
await window.waitForLoadState('networkidle');
62+
await window.waitForTimeout(2_000);
63+
return { window, close: async () => {
7264
await electronApp.close();
73-
} finally {
7465
await fs.remove(userDataDir);
75-
}
76-
}
77-
78-
export const startVSCode = async (workspaceDir: string) => {
79-
66+
} };
8067
}
8168

8269
test('Lineage panel renders correctly - no project path config (default)', async () => {
8370
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-'));
8471
await fs.copy(SUSHI_SOURCE_PATH, tempDir);
85-
8672
try {
87-
await testLineageWithProjectPath(tempDir, tempDir);
88-
} finally {
73+
const { window, close } = await startVSCode(tempDir);
74+
await testLineageWithProjectPath(window);
75+
await close();
76+
} finally {
8977
await fs.remove(tempDir);
9078
}
9179
});
9280

9381
test('Lineage panel renders correctly - relative project path', async () => {
94-
// Create workspace directory with subdirectory containing the project
9582
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
96-
const projectSubdir = path.join(workspaceDir, 'projects', 'sushi');
97-
await fs.ensureDir(path.dirname(projectSubdir));
98-
await fs.copy(SUSHI_SOURCE_PATH, projectSubdir);
83+
const projectDir = path.join(workspaceDir, 'projects', 'sushi');
84+
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
85+
86+
const settings = {
87+
"sqlmesh.projectPath": path.relative(workspaceDir, projectDir),
88+
};
89+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
90+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
9991

10092
try {
101-
// Test with relative path
102-
await testLineageWithProjectPath(workspaceDir, projectSubdir, 'projects/sushi');
93+
const { window, close } = await startVSCode(workspaceDir);
94+
await testLineageWithProjectPath(window);
95+
await close();
10396
} finally {
10497
await fs.remove(workspaceDir);
10598
}
10699
});
107100

108101
test('Lineage panel renders correctly - absolute project path', async () => {
109-
// Create workspace directory
110102
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
111-
112-
// Create project directory outside workspace
113-
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-project-'));
103+
const projectDir = path.join(workspaceDir, 'projects', 'sushi');
104+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
114105
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
106+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
107+
const settings = {
108+
"sqlmesh.projectPath": projectDir,
109+
};
110+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
115111

116112
try {
117-
// Test with absolute path
118-
await testLineageWithProjectPath(workspaceDir, projectDir, projectDir);
113+
const { window, close } = await startVSCode(workspaceDir);
114+
await testLineageWithProjectPath(window);
115+
await close();
119116
} finally {
120117
await fs.remove(workspaceDir);
121-
await fs.remove(projectDir);
122118
}
123119
});

0 commit comments

Comments
 (0)