diff --git a/package-lock.json b/package-lock.json index 9bc8b64..a35b87d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "@groqfmt/wasm": "^1.2.2", "@sanity/client": "^6.21.1", "groq-js": "^1.12.0", "line-number": "^0.1.0", @@ -43,6 +44,11 @@ "node": ">=12" } }, + "node_modules/@groqfmt/wasm": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@groqfmt/wasm/-/wasm-1.2.2.tgz", + "integrity": "sha512-v79EIihpH6/tjsTq3UzQc3Gkoi22DHOVxIekOCJZF6HROWXeNgekWTkEjkiWpH02vgzulJprLozPr/ar99vrIA==" + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", diff --git a/package.json b/package.json index 09edcf0..48e0838 100644 --- a/package.json +++ b/package.json @@ -150,5 +150,9 @@ "env:source": "export $(cat .envrc | xargs)", "vsce:publish": "sh publish.sh", "upgrade-interactive": "npx npm-check -u" - } + }, + "files": [ + "out/**/*", + "vendor/**/*" + ] } diff --git a/src/extension.ts b/src/extension.ts index 82e0e26..97aa376 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,7 @@ import {Config} from './config/findConfig' import {GroqContentProvider} from './providers/content-provider' import {GROQCodeLensProvider} from './providers/groq-codelens-provider' import {executeGroq} from './query' +import {formatGroq} from './format' export function activate(context: vscode.ExtensionContext) { // needed to load sanity.cli.ts @@ -88,6 +89,21 @@ export function activate(context: vscode.ExtensionContext) { }) context.subscriptions.push(disposable) + context.subscriptions.push( + vscode.commands.registerCommand('sanity.formatGroq', async (groqQuery: string, uri: string, range: vscode.Range) => { + const document = await vscode.workspace.openTextDocument(vscode.Uri.file(uri)); + const query = groqQuery; + const formattedQuery = await formatGroq(query); // Await the async function + + const edit = new vscode.WorkspaceEdit(); + edit.replace(document.uri, range, formattedQuery); + await vscode.workspace.applyEdit(edit); + + // Refresh the CodeLens + await vscode.commands.executeCommand('vscode.executeCodeLensProvider', document.uri); + }) + ); + function readConfig() { const settings = vscode.workspace.getConfiguration('sanity') openJSONFile = settings.get('openJSONFile', false) diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..f8a623e --- /dev/null +++ b/src/format.ts @@ -0,0 +1,15 @@ +import { loadWasm } from "./wasm-loader" + +let isWasmLoaded = false; + +async function formatGroq(groq: string): Promise { + if (!isWasmLoaded) { + await loadWasm(); + isWasmLoaded = true; + } + + const formattedGroq = (global as any).groqfmt(groq); // Assuming groqfmt is exposed globally by the WASM module + return formattedGroq.result; +} + +export { formatGroq }; diff --git a/src/providers/groq-codelens-provider.ts b/src/providers/groq-codelens-provider.ts index 59b72ff..0db2fda 100644 --- a/src/providers/groq-codelens-provider.ts +++ b/src/providers/groq-codelens-provider.ts @@ -3,54 +3,55 @@ import {type CodeLensProvider, type TextDocument, type CancellationToken, CodeLe interface ExtractedQuery { content: string uri: string - position: Position + range: Range; } function extractAllTemplateLiterals(document: TextDocument): ExtractedQuery[] { - const documents: ExtractedQuery[] = [] - const text = document.getText() - const regExpGQL = new RegExp('groq\\s*`([\\s\\S]+?)`', 'mg') + const documents: ExtractedQuery[] = []; + const text = document.getText(); + const regExpGQL = new RegExp('groq\\s*`([\\s\\S]+?)`', 'mg'); - let prevIndex = 0 - let result + let result; while ((result = regExpGQL.exec(text)) !== null) { - const content = result[1] - const queryPosition = text.indexOf(content, prevIndex) + const content = result[1]; + const startPosition = document.positionAt(result.index + result[0].indexOf(content)); + const endPosition = document.positionAt(result.index + result[0].indexOf(content) + content.length); + const range = new Range(startPosition, endPosition); documents.push({ content: content, uri: document.uri.path, - position: document.positionAt(queryPosition), - }) - prevIndex = queryPosition + 1 + range: range, + }); } - return documents + return documents; } function extractAllDefineQuery(document: TextDocument): ExtractedQuery[] { - const documents: ExtractedQuery[] = [] - const text = document.getText() - const pattern = '(\\s*defineQuery\\((["\'`])([\\s\\S]*?)\\2\\))' + const documents: ExtractedQuery[] = []; + const text = document.getText(); + const pattern = '(\\s*defineQuery\\((["\'`])([\\s\\S]*?)\\2\\))'; const regexp = new RegExp(pattern, 'g'); - let prevIndex = 0 - let result + let result; while ((result = regexp.exec(text)) !== null) { - const content = result[3] - const queryPosition = text.indexOf(result[1], prevIndex) + const content = result[3]; + const startPosition = document.positionAt(result.index + result[0].indexOf(content)); + const endPosition = document.positionAt(result.index + result[0].indexOf(content) + content.length); + const range = new Range(startPosition, endPosition); documents.push({ content: content, uri: document.uri.path, - position: document.positionAt(queryPosition), - }) - prevIndex = queryPosition + 1 + range: range, + }); } - return documents + return documents; } export class GROQCodeLensProvider implements CodeLensProvider { constructor() {} public provideCodeLenses(document: TextDocument, _token: CancellationToken): CodeLens[] { + if (document.languageId === 'groq') { return [ new CodeLens(new Range(new Position(0, 0), new Position(0, 0)), { @@ -58,22 +59,37 @@ export class GROQCodeLensProvider implements CodeLensProvider { command: 'sanity.executeGroq', arguments: [document.getText()], }), - ] + new CodeLens(new Range(new Position(0, 0), new Position(0, 0)), { + title: 'Format Query', + command: 'sanity.formatGroq', + arguments: [document.getText(), document.uri, new Position(0, 0)], + }), + ]; } // find all lines where "groq" exists - const queries: ExtractedQuery[] = [...extractAllTemplateLiterals(document), ...extractAllDefineQuery(document)] + const queries: ExtractedQuery[] = [...extractAllTemplateLiterals(document), ...extractAllDefineQuery(document)]; // add a button above each line that has groq - return queries.map((def) => { - return new CodeLens( - new Range(new Position(def.position.line, 0), new Position(def.position.line, 0)), + const lenses: CodeLens[] = []; + queries.forEach((def) => { + lenses.push(new CodeLens( + def.range, { title: 'Execute Query', command: 'sanity.executeGroq', arguments: [def.content], } - ) - }) + )); + lenses.push(new CodeLens( + def.range, + { + title: 'Format Query', + command: 'sanity.formatGroq', + arguments: [def.content, def.uri, def.range], + } + )); + }); + return lenses; } } diff --git a/src/wasm-loader.ts b/src/wasm-loader.ts new file mode 100644 index 0000000..8d23a8b --- /dev/null +++ b/src/wasm-loader.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; + +import '@groqfmt/wasm/dist/wasm-exec'; + +const go = new (global as any).Go(); + +async function loadWasm() { + const wasmPath = path.resolve(__dirname, '../vendor/groqfmt.wasm'); + + const wasmData = await util.promisify(fs.readFile)(wasmPath); + const { instance } = await WebAssembly.instantiate(wasmData, go.importObject); + go.run(instance); +} + +export { loadWasm }; diff --git a/tsconfig.json b/tsconfig.json index 6056383..23dc6e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,9 @@ "esModuleInterop": true, "jsx": "react" }, - "exclude": ["node_modules", ".vscode-test", "ts-graphql-plugin"] + "exclude": ["node_modules", ".vscode-test", "ts-graphql-plugin"], + "include": [ + "src/**/*.ts", + "vendor/groqfmt.wasm" + ] } diff --git a/vendor/groqfmt.wasm b/vendor/groqfmt.wasm new file mode 100755 index 0000000..89196af Binary files /dev/null and b/vendor/groqfmt.wasm differ