From d6b611d3c1d519194269032648fad94edfaaf02e Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 17 Jan 2026 19:49:59 -0500 Subject: [PATCH] ide: code completion in playground & prettier upgrade --- .prettierrc.js | 4 +- .vscode/extensions/squawk-dev/esbuild.js | 6 +- .../squawk-dev/src/definitionProviders.ts | 16 ++--- .../extensions/squawk-dev/src/extension.ts | 16 ++--- PLAN.md | 4 -- crates/squawk_ide/src/completion.rs | 2 +- crates/squawk_wasm/src/lib.rs | 47 ++++++++++++++ js/install.js | 10 +-- package.json | 2 +- playground/src/App.tsx | 10 +++ playground/src/providers.tsx | 63 +++++++++++++++++++ playground/src/squawk.tsx | 18 ++++++ s/prettier | 4 +- squawk-vscode/esbuild.js | 6 +- yarn.lock | 8 +-- 15 files changed, 175 insertions(+), 41 deletions(-) diff --git a/.prettierrc.js b/.prettierrc.js index 57ecd10d..59a6a6c9 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -4,8 +4,8 @@ module.exports = { useTabs: false, tabWidth: 2, singleQuote: false, - trailingComma: "es5", + trailingComma: "all", bracketSpacing: true, jsxBracketSameLine: true, - arrowParens: "avoid", + arrowParens: "always", } diff --git a/.vscode/extensions/squawk-dev/esbuild.js b/.vscode/extensions/squawk-dev/esbuild.js index f3d6c11b..8a7e564b 100644 --- a/.vscode/extensions/squawk-dev/esbuild.js +++ b/.vscode/extensions/squawk-dev/esbuild.js @@ -13,11 +13,11 @@ const esbuildProblemMatcherPlugin = { build.onStart(() => { console.log("[watch] build started") }) - build.onEnd(result => { + build.onEnd((result) => { result.errors.forEach(({ text, location }) => { console.error(`✘ [ERROR] ${text}`) console.error( - ` ${location.file}:${location.line}:${location.column}:` + ` ${location.file}:${location.line}:${location.column}:`, ) }) console.log("[watch] build finished") @@ -50,7 +50,7 @@ async function main() { } } -main().catch(e => { +main().catch((e) => { console.error(e) process.exit(1) }) diff --git a/.vscode/extensions/squawk-dev/src/definitionProviders.ts b/.vscode/extensions/squawk-dev/src/definitionProviders.ts index 33bf2e34..cc10ae16 100644 --- a/.vscode/extensions/squawk-dev/src/definitionProviders.ts +++ b/.vscode/extensions/squawk-dev/src/definitionProviders.ts @@ -8,7 +8,7 @@ export class TestSnapshotDefinitionProvider public async provideDefinition( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken + token: vscode.CancellationToken, ): Promise { // crates/parser/src/snapshots/parser__alter_table_test__parse_alter_column.snap const currentFilePath = document.uri.fsPath @@ -25,7 +25,7 @@ export class TestSnapshotDefinitionProvider const workspaceRoot = path.dirname(cratesDir) const header = document.getText( - new Range(new Position(0, 0), new Position(3, 0)) + new Range(new Position(0, 0), new Position(3, 0)), ) let inputFilePathRel: string | null = null for (const line of header.split("\n")) { @@ -47,7 +47,7 @@ export class TestSnapshotDefinitionProvider function getCursorPosition( document: vscode.TextDocument, - position: Position + position: Position, ): [number | null, number | null] { const cursorLine = document.lineAt(position.line).text @@ -84,13 +84,13 @@ function getCursorPosition( async function testFuncLocation( testFilePath: vscode.Uri, cursorPosition: Position, - document: vscode.TextDocument + document: vscode.TextDocument, ): Promise { const testFileDoc = await vscode.workspace.openTextDocument(testFilePath) const [byteOffsetStart, byteOffsetEnd] = getCursorPosition( document, - cursorPosition + cursorPosition, ) const destFunctionPositionStart = testFileDoc.positionAt(byteOffsetStart!) @@ -116,7 +116,7 @@ async function testFuncLocation( const originSelectionRange = new Range( new Position(cursorPosition.line, leadingWhiteSpaceEnd), - new Position(cursorPosition.line, cursorLine.length) + new Position(cursorPosition.line, cursorLine.length), ) return [ @@ -125,11 +125,11 @@ async function testFuncLocation( targetUri: testFilePath, targetRange: new Range( destFunctionPositionStart, - destFunctionPositionEnd + destFunctionPositionEnd, ), targetSelectionRange: new Range( destFunctionPositionStart, - destFunctionPositionEnd + destFunctionPositionEnd, ), } satisfies LocationLink, ] diff --git a/.vscode/extensions/squawk-dev/src/extension.ts b/.vscode/extensions/squawk-dev/src/extension.ts index 82f5d0ac..123eb4ef 100644 --- a/.vscode/extensions/squawk-dev/src/extension.ts +++ b/.vscode/extensions/squawk-dev/src/extension.ts @@ -19,7 +19,7 @@ function computeSnapshotPath(sqlPath: string): string | undefined { // crates/parser/src/snapshots/parser__test__alter_foreign_table.snap return path.join( sourceRoot, - `src/snapshots/parser__test__${testName}_${ok_or_err}.snap` + `src/snapshots/parser__test__${testName}_${ok_or_err}.snap`, ) } @@ -27,7 +27,7 @@ function computeSnapshotPath(sqlPath: string): string | undefined { class JumpToSnapshotCodeLensProvider implements vscode.CodeLensProvider { public provideCodeLenses( document: vscode.TextDocument, - token: vscode.CancellationToken + token: vscode.CancellationToken, ): vscode.CodeLens[] | Thenable { const position = new vscode.Position(0, 0) const range = new vscode.Range(position, position) @@ -52,8 +52,8 @@ export function activate(context: vscode.ExtensionContext) { pattern: "**/snapshots/*.snap", }, ], - new TestSnapshotDefinitionProvider() - ) + new TestSnapshotDefinitionProvider(), + ), ) context.subscriptions.push( @@ -64,8 +64,8 @@ export function activate(context: vscode.ExtensionContext) { // pattern: "**/test_data/**/*.sql", // pattern: "*.sql", }, - new JumpToSnapshotCodeLensProvider() - ) + new JumpToSnapshotCodeLensProvider(), + ), ) context.subscriptions.push( @@ -73,8 +73,8 @@ export function activate(context: vscode.ExtensionContext) { "squawk-dev.jumpToSnapshot", (path: string) => { openSnapshotPath(path) - } - ) + }, + ), ) } diff --git a/PLAN.md b/PLAN.md index 9e0ed374..79933de4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -35,7 +35,6 @@ support SQL embedded in other languages - https://fljd.in/en/2024/11/25/substituting-a-variable-in-a-sql-script/ - https://www.timescale.com/blog/how-to-build-an-iot-pipeline-for-real-time-analytics-in-postgresql - - `$__timeFrom()`, `$__timeTo()`, and `$sensor_id` - https://grafana.com/docs/grafana/latest/dashboards/variables/ @@ -1022,15 +1021,12 @@ size = 24 (0x18), align = 0x8, needs Drop other fields? - alignment - - https://r.ena.to/blog/optimizing-postgres-table-layout-for-maximum-efficiency/ - common values and distribution - - https://observablehq.com/documentation/cells/data-table#data-table-cell - index size on hover - - https://www.peterbe.com/plog/index-size-postgresql - data staleness -- if you have a daily batch job to calculate data, we could expose the staleness date diff --git a/crates/squawk_ide/src/completion.rs b/crates/squawk_ide/src/completion.rs index 5b3070fb..cb0145bb 100644 --- a/crates/squawk_ide/src/completion.rs +++ b/crates/squawk_ide/src/completion.rs @@ -192,7 +192,7 @@ fn default_completions() -> Vec { detail: None, insert_text: Some(format!("{stmt} $0;")), insert_text_format: Some(CompletionInsertTextFormat::Snippet), - trigger_completion_after_insert: false, + trigger_completion_after_insert: true, }) .into_iter() .collect() diff --git a/crates/squawk_wasm/src/lib.rs b/crates/squawk_wasm/src/lib.rs index 83df6da1..b4481208 100644 --- a/crates/squawk_wasm/src/lib.rs +++ b/crates/squawk_wasm/src/lib.rs @@ -533,3 +533,50 @@ struct WasmSelectionRange { end_line: u32, end_column: u32, } + +#[wasm_bindgen] +pub fn completion(content: String, line: u32, col: u32) -> Result { + let parse = squawk_syntax::SourceFile::parse(&content); + let line_index = LineIndex::new(&content); + let offset = position_to_offset(&line_index, line, col)?; + let items = squawk_ide::completion::completion(&parse.tree(), offset); + + let converted: Vec = items + .into_iter() + .map(|item| WasmCompletionItem { + label: item.label, + kind: match item.kind { + squawk_ide::completion::CompletionItemKind::Keyword => "keyword", + squawk_ide::completion::CompletionItemKind::Table => "table", + squawk_ide::completion::CompletionItemKind::Column => "column", + squawk_ide::completion::CompletionItemKind::Function => "function", + squawk_ide::completion::CompletionItemKind::Schema => "schema", + squawk_ide::completion::CompletionItemKind::Type => "type", + squawk_ide::completion::CompletionItemKind::Snippet => "snippet", + } + .to_string(), + detail: item.detail, + insert_text: item.insert_text, + insert_text_format: item.insert_text_format.map(|fmt| { + match fmt { + squawk_ide::completion::CompletionInsertTextFormat::PlainText => "plainText", + squawk_ide::completion::CompletionInsertTextFormat::Snippet => "snippet", + } + .to_string() + }), + trigger_completion_after_insert: item.trigger_completion_after_insert, + }) + .collect(); + + serde_wasm_bindgen::to_value(&converted).map_err(into_error) +} + +#[derive(Serialize)] +struct WasmCompletionItem { + label: String, + kind: String, + detail: Option, + insert_text: Option, + insert_text_format: Option, + trigger_completion_after_insert: bool, +} diff --git a/js/install.js b/js/install.js index 389fca32..ea1f3fd8 100644 --- a/js/install.js +++ b/js/install.js @@ -87,7 +87,7 @@ function getCachedPath(url) { return path.join( getNpmCache(), "squawk-cli", - `${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, "-")}` + `${digest}-${path.basename(url).replace(/[^a-zA-Z0-9.]+/g, "-")}`, ) } @@ -134,7 +134,7 @@ function downloadBinary() { "accept-encoding": "gzip, deflate, br", }, redirect: "follow", - }).then(response => { + }).then((response) => { if (!response.ok) { throw new Error(`Received ${response.status}: ${response.statusText}`) } @@ -147,10 +147,10 @@ function downloadBinary() { return new Promise((resolve, reject) => { response.body - .on("error", e => reject(e)) + .on("error", (e) => reject(e)) .pipe(decompressor) .pipe(fs.createWriteStream(tempPath, { mode: 0o755 })) - .on("error", e => reject(e)) + .on("error", (e) => reject(e)) .on("close", () => resolve()) }).then(() => { fs.copyFileSync(tempPath, cachedPath) @@ -162,7 +162,7 @@ function downloadBinary() { downloadBinary() .then(() => process.exit(0)) - .catch(e => { + .catch((e) => { console.error(e) process.exit(1) }) diff --git a/package.json b/package.json index cb8e395d..8b2e39c7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@typescript-eslint/parser": "^3.3.0", "eslint": "^7.2.0", "eslint-plugin-import": "^2.21.2", - "prettier": "^2.0.5", + "prettier": "^3.8.0", "typescript": "^3.9.5" }, "dependencies": { diff --git a/playground/src/App.tsx b/playground/src/App.tsx index ef0448fd..a893c9c1 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -20,6 +20,7 @@ import { provideReferences, provideDocumentSymbols, provideSelectionRanges, + provideCompletionItems, } from "./providers" const modes = ["Lint", "Syntax Tree", "Tokens"] as const @@ -417,6 +418,14 @@ function registerMonacoProviders() { provideSelectionRanges, }) + const completionProvider = monaco.languages.registerCompletionItemProvider( + "pgsql", + { + triggerCharacters: ["."], + provideCompletionItems, + }, + ) + return () => { languageConfig.dispose() codeActionProvider.dispose() @@ -426,6 +435,7 @@ function registerMonacoProviders() { documentSymbolProvider.dispose() inlayHintsProvider.dispose() selectionRangeProvider.dispose() + completionProvider.dispose() tokenProvider.dispose() } } diff --git a/playground/src/providers.tsx b/playground/src/providers.tsx index fc6dbcc1..79ed9b68 100644 --- a/playground/src/providers.tsx +++ b/playground/src/providers.tsx @@ -1,6 +1,7 @@ import * as monaco from "monaco-editor" import { code_actions, + completion, document_symbols, find_references, goto_definition, @@ -243,3 +244,65 @@ export async function provideSelectionRanges( return [] } } + +function convertCompletionKind( + kind: string, +): monaco.languages.CompletionItemKind { + switch (kind) { + case "keyword": + return monaco.languages.CompletionItemKind.Keyword + case "table": + return monaco.languages.CompletionItemKind.Class + case "column": + return monaco.languages.CompletionItemKind.Field + case "function": + return monaco.languages.CompletionItemKind.Function + case "schema": + return monaco.languages.CompletionItemKind.Module + case "type": + return monaco.languages.CompletionItemKind.TypeParameter + case "snippet": + return monaco.languages.CompletionItemKind.Snippet + default: + return monaco.languages.CompletionItemKind.Text + } +} + +export async function provideCompletionItems( + model: monaco.editor.ITextModel, + position: monaco.Position, +): Promise { + const content = model.getValue() + if (!content) return { suggestions: [] } + + try { + const items = completion( + content, + position.lineNumber - 1, + position.column - 1, + ) + + const suggestions: monaco.languages.CompletionItem[] = items.map( + (item) => ({ + label: item.label, + kind: convertCompletionKind(item.kind), + detail: item.detail ?? undefined, + insertText: item.insert_text ?? item.label, + insertTextRules: + item.insert_text_format === "snippet" + ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet + : undefined, + command: item.trigger_completion_after_insert + ? { id: "editor.action.triggerSuggest", title: "Trigger Suggest" } + : undefined, + sortText: item.kind === "schema" ? `z${item.label}` : item.label, + range: undefined as unknown as monaco.IRange, + }), + ) + + return { suggestions } + } catch (e) { + console.error("Error in provideCompletionItems:", e) + return { suggestions: [] } + } +} diff --git a/playground/src/squawk.tsx b/playground/src/squawk.tsx index e906c4ee..44ad0277 100644 --- a/playground/src/squawk.tsx +++ b/playground/src/squawk.tsx @@ -10,6 +10,7 @@ import initWasm, { code_actions as code_actions_, inlay_hints as inlay_hints_, selection_ranges as selection_ranges_, + completion as completion_, } from "./pkg/squawk_wasm" export type TextEdit = { @@ -90,6 +91,14 @@ export function selection_ranges( return selection_ranges_(content, positions) } +export function completion( + content: string, + line: number, + column: number, +): CompletionItem[] { + return completion_(content, line, column) +} + export function useErrors(text: string) { const isReady = useWasmStatus() return isReady ? lint(text) : [] @@ -179,3 +188,12 @@ export interface SelectionRange { end_line: number end_column: number } + +export interface CompletionItem { + label: string + kind: string + detail: string | null + insert_text: string | null + insert_text_format: string | null + trigger_completion_after_insert: boolean +} diff --git a/s/prettier b/s/prettier index 05be9f71..304360ea 100755 --- a/s/prettier +++ b/s/prettier @@ -4,9 +4,9 @@ set -ex main() { if [ -n "$CI" ]; then - ./node_modules/.bin/prettier --check '**/*.{js,md,yml,json}' + ./node_modules/.bin/prettier --check '**/*.{js,tsx,ts,md,yml,json}' else - ./node_modules/.bin/prettier '**/*.{js,md,yml,json}' --write + ./node_modules/.bin/prettier '**/*.{js,tsx,ts,md,yml,json}' --write fi } diff --git a/squawk-vscode/esbuild.js b/squawk-vscode/esbuild.js index f3d6c11b..8a7e564b 100644 --- a/squawk-vscode/esbuild.js +++ b/squawk-vscode/esbuild.js @@ -13,11 +13,11 @@ const esbuildProblemMatcherPlugin = { build.onStart(() => { console.log("[watch] build started") }) - build.onEnd(result => { + build.onEnd((result) => { result.errors.forEach(({ text, location }) => { console.error(`✘ [ERROR] ${text}`) console.error( - ` ${location.file}:${location.line}:${location.column}:` + ` ${location.file}:${location.line}:${location.column}:`, ) }) console.log("[watch] build finished") @@ -50,7 +50,7 @@ async function main() { } } -main().catch(e => { +main().catch((e) => { console.error(e) process.exit(1) }) diff --git a/yarn.lock b/yarn.lock index bdae035f..0561e181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1066,10 +1066,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" - integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== +prettier@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.0.tgz#f72cf71505133f40cfa2ef77a2668cdc558fcd69" + integrity sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA== progress@^2.0.0: version "2.0.3"