Skip to content

Commit 123b1bf

Browse files
committed
feat(lsp): clear hints on refresh
1 parent 9e5bf54 commit 123b1bf

2 files changed

Lines changed: 231 additions & 5 deletions

File tree

sqlmesh/lsp/main.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]:
100100
context = self.context_class(paths=paths)
101101
lsp_context = LSPContext(context)
102102
self.lsp_context = lsp_context
103+
104+
# Clear inlay hints by requesting a refresh from the client
105+
self.server.send_notification(types.WORKSPACE_INLAY_HINT_REFRESH)
106+
103107
return lsp_context
104108
except Exception as e:
105109
self.server.log_trace(f"Error creating context: {e}")
@@ -694,15 +698,15 @@ def _ensure_context_for_document(
694698
for a config.py or config.yml file in the parent directories.
695699
"""
696700
if self.lsp_context is not None:
697-
context = self.lsp_context
698-
context.context.load() # Reload or refresh context
699-
self.lsp_context = LSPContext(context.context)
700-
return
701+
# If we already have a context, refresh it
702+
paths = list(self.lsp_context.context.configs)
703+
self._create_lsp_context(paths)
704+
return None
701705

702706
# No context yet: try to find config and load it
703707
path = document_uri.to_path()
704708
if path.suffix not in (".sql", ".py"):
705-
return
709+
return None
706710

707711
loaded = False
708712
# Ascend directories to look for config
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { test, expect, Page } from '@playwright/test'
2+
import path from 'path'
3+
import fs from 'fs-extra'
4+
import os from 'os'
5+
import { startVSCode, SUSHI_SOURCE_PATH } from './utils'
6+
7+
/**
8+
* Helper function to create a broken SQLMesh project
9+
*/
10+
async function createBrokenProject(tempDir: string): Promise<void> {
11+
// Copy the sushi project as a base
12+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
13+
14+
// Create a broken config.py that will fail to load
15+
const brokenConfig = `
16+
# This config will fail to load due to import error
17+
from non_existent_module import something_that_doesnt_exist
18+
19+
# Rest of the config would be here but it won't get executed
20+
model_defaults = {}
21+
`
22+
await fs.writeFile(path.join(tempDir, 'config.py'), brokenConfig)
23+
}
24+
25+
/**
26+
* Helper function to create a project with invalid SQL that will cause linting errors
27+
*/
28+
async function createProjectWithInvalidSQL(tempDir: string): Promise<void> {
29+
// Copy the sushi project as a base
30+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
31+
32+
// Create a model with invalid SQL syntax
33+
const invalidSQL = `
34+
MODEL (
35+
name sushi.broken_model,
36+
kind FULL
37+
);
38+
39+
-- This SQL has syntax errors that should trigger diagnostics
40+
SELECT
41+
invalid_column_that_doesnt_exist,
42+
FROM non_existent_table
43+
WHERE invalid syntax here
44+
`
45+
await fs.writeFile(path.join(tempDir, 'models', 'broken_model.sql'), invalidSQL)
46+
}
47+
48+
/**
49+
* Test that failing projects don't spam the user with excessive error messages
50+
*/
51+
test('Failing project - broken config does not spam user with errors', async () => {
52+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-broken-'))
53+
await createBrokenProject(tempDir)
54+
55+
try {
56+
const { window, close } = await startVSCode(tempDir)
57+
58+
// Try to trigger lineage command which would normally load the context
59+
await window.keyboard.press(
60+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
61+
)
62+
await window.keyboard.type('Lineage: Focus On View')
63+
await window.keyboard.press('Enter')
64+
65+
// Wait a reasonable amount of time for any error messages to appear
66+
await window.waitForTimeout(3000)
67+
68+
// Check that we don't see excessive error notifications
69+
// We expect at most one error message about context loading failure
70+
const errorNotifications = window.locator('.notifications-center .notification-list-item.error')
71+
const errorCount = await errorNotifications.count()
72+
73+
// Should have at most 1-2 error notifications, not a spam of them
74+
expect(errorCount).toBeLessThanOrEqual(2)
75+
76+
// Verify that no "Loaded SQLMesh context" success message appears
77+
const loadedContextText = window.locator('text=Loaded SQLMesh context')
78+
await expect(loadedContextText).not.toBeVisible({ timeout: 2000 })
79+
80+
await close()
81+
} finally {
82+
await fs.remove(tempDir)
83+
}
84+
})
85+
86+
/**
87+
* Test that projects with invalid SQL show diagnostics but don't crash the LSP
88+
*/
89+
test('Failing project - invalid SQL shows diagnostics without crashing', async () => {
90+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-invalid-sql-'))
91+
await createProjectWithInvalidSQL(tempDir)
92+
93+
try {
94+
const { window, close } = await startVSCode(tempDir)
95+
96+
// Open the broken SQL file
97+
await window.locator('text=broken_model.sql').click()
98+
99+
// Wait for the file to open and the LSP to process it
100+
await window.waitForTimeout(2000)
101+
102+
// Check that we have some diagnostic errors (red squiggles) in the editor
103+
// This validates that the LSP is working and providing diagnostics
104+
const diagnosticMarkers = window.locator('.monaco-editor .squiggly-error, .monaco-editor .squiggly-warning')
105+
await expect(diagnosticMarkers.first()).toBeVisible({ timeout: 5000 })
106+
107+
// Verify that the problems panel shows issues
108+
await window.keyboard.press(
109+
process.platform === 'darwin' ? 'Meta+Shift+M' : 'Control+Shift+M',
110+
)
111+
112+
// Should see problems in the problems panel
113+
const problemsPanel = window.locator('.panel .problems-panel')
114+
await expect(problemsPanel).toBeVisible({ timeout: 3000 })
115+
116+
// Check that there are no excessive error notifications in the UI
117+
const errorNotifications = window.locator('.notifications-center .notification-list-item.error')
118+
const errorCount = await errorNotifications.count()
119+
expect(errorCount).toBeLessThanOrEqual(3) // Allow for a few errors but not spam
120+
121+
await close()
122+
} finally {
123+
await fs.remove(tempDir)
124+
}
125+
})
126+
127+
/**
128+
* Test that project loading failures are handled gracefully without hanging
129+
*/
130+
test('Failing project - graceful handling of context loading failures', async () => {
131+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-context-fail-'))
132+
await createBrokenProject(tempDir)
133+
134+
try {
135+
const { window, close } = await startVSCode(tempDir)
136+
137+
// Try various LSP operations that would require context loading
138+
const operations = [
139+
async () => {
140+
// Try hover
141+
await window.locator('text=config.py').click()
142+
await window.waitForTimeout(500)
143+
const editor = window.locator('.monaco-editor')
144+
await editor.hover()
145+
},
146+
async () => {
147+
// Try opening a SQL file
148+
await window.locator('text=customers.sql').click()
149+
await window.waitForTimeout(500)
150+
},
151+
async () => {
152+
// Try formatting command
153+
await window.keyboard.press(
154+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
155+
)
156+
await window.keyboard.type('Format Document')
157+
await window.keyboard.press('Escape') // Cancel the command
158+
}
159+
]
160+
161+
// Execute operations and ensure none hang indefinitely
162+
for (const operation of operations) {
163+
const timeoutPromise = new Promise((_, reject) =>
164+
setTimeout(() => reject(new Error('Operation timed out')), 5000)
165+
)
166+
167+
await Promise.race([
168+
operation().catch(() => {
169+
// Operations may fail, that's expected for broken projects
170+
}),
171+
timeoutPromise
172+
])
173+
}
174+
175+
// Verify VSCode is still responsive
176+
await window.keyboard.press(
177+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
178+
)
179+
await window.keyboard.press('Escape')
180+
181+
await close()
182+
} finally {
183+
await fs.remove(tempDir)
184+
}
185+
})
186+
187+
/**
188+
* Test that repeated operations on failing projects don't accumulate errors
189+
*/
190+
test('Failing project - repeated operations do not accumulate error spam', async () => {
191+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-repeated-'))
192+
await createBrokenProject(tempDir)
193+
194+
try {
195+
const { window, close } = await startVSCode(tempDir)
196+
197+
// Perform the same operation multiple times
198+
for (let i = 0; i < 3; i++) {
199+
await window.keyboard.press(
200+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
201+
)
202+
await window.keyboard.type('Lineage: Focus On View')
203+
await window.keyboard.press('Enter')
204+
await window.waitForTimeout(1000)
205+
206+
// Close any command palette that might be open
207+
await window.keyboard.press('Escape')
208+
}
209+
210+
// Check that error notifications haven't accumulated excessively
211+
const errorNotifications = window.locator('.notifications-center .notification-list-item.error')
212+
const errorCount = await errorNotifications.count()
213+
214+
// Should not have accumulated errors for each repeated operation
215+
// Allow for a reasonable number but ensure it's not 3x the number of operations
216+
expect(errorCount).toBeLessThanOrEqual(4)
217+
218+
await close()
219+
} finally {
220+
await fs.remove(tempDir)
221+
}
222+
})

0 commit comments

Comments
 (0)