From 671eb0dc99ae5dfdc273e7f8000eea51831f77bc Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 23 Jan 2024 22:08:11 +0100 Subject: [PATCH] add code action to vscode extension for opening the local EdgeDB UI --- vscode-extension/CHANGELOG.md | 3 + vscode-extension/package-lock.json | 12 ++ vscode-extension/package.json | 1 + vscode-extension/src/extension.ts | 182 ++++++++++++++++++++++++++++- 4 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 vscode-extension/CHANGELOG.md diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md new file mode 100644 index 0000000..8036b4f --- /dev/null +++ b/vscode-extension/CHANGELOG.md @@ -0,0 +1,3 @@ +# main + +- Add code action for opening queries in the local EdgeDB UI. diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 42490ba..2b84b48 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "dotenv": "^16.3.2", "typescript": "5.0.4" }, "devDependencies": { @@ -48,6 +49,17 @@ "integrity": "sha512-MWFN5R7a33n8eJZJmdVlifjig3LWUNRrPeO1xemIcZ0ae0TEQuRc7G2xV0LUX78RZFECY1plYBn+dP/Acc3L0Q==", "dev": true }, + "node_modules/dotenv": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.17.15", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz", diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 26479fe..55893fc 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -33,6 +33,7 @@ }, "license": "MIT", "dependencies": { + "dotenv": "^16.3.2", "typescript": "5.0.4" }, "devDependencies": { diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 0bc0f44..094886d 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1,5 +1,8 @@ -import { readFileSync } from "fs"; -import path from "path"; +import * as childProcess from "child_process"; +import * as os from "os"; +import * as fs from "fs"; +import * as path from "path"; +import * as dotenv from "dotenv"; import { ExtensionContext, workspace, @@ -9,12 +12,36 @@ import { DiagnosticSeverity, Range, Position, + TextDocument, + window, + commands, + env, } from "vscode"; -export async function activate(context: ExtensionContext) { - let currentWorkspacePath = workspace.workspaceFolders?.[0].uri.fsPath; - if (currentWorkspacePath == null) throw new Error("Init failed."); +let tempFilePrefix = "rescript_edgedb_" + process.pid + "_"; +let tempFileId = 0; +function createFileInTempDir() { + let tempFileName = tempFilePrefix + tempFileId + ".res"; + tempFileId = tempFileId + 1; + return path.join(os.tmpdir(), tempFileName); +} + +function findProjectPackageJsonRoot(source: string): null | string { + let dir = path.dirname(source); + if (fs.existsSync(path.join(dir, "package.json"))) { + return dir; + } else { + if (dir === source) { + // reached top + return null; + } else { + return findProjectPackageJsonRoot(dir); + } + } +} + +async function setupErrorLogWatcher(context: ExtensionContext) { const rootFiles = await workspace.findFiles( "**/{bsconfig,rescript}.json", "**/node_modules/**", @@ -40,7 +67,7 @@ export async function activate(context: ExtensionContext) { function syncDiagnostics() { try { - const contents = readFileSync( + const contents = fs.readFileSync( path.resolve(dir, ".generator.edgeql.log"), "utf-8" ); @@ -113,4 +140,147 @@ export async function activate(context: ExtensionContext) { ); } +type dataFromFile = { + content: string; + start: { line: number; col: number }; + end: { line: number; col: number }; + tag: string; +}; + +const edgeqlContent: Map = new Map(); + +function callRescriptEdgeDBCli(command: string[], cwd: string) { + const dotEnvLoc = path.join(cwd, ".env"); + const dotEnvExists = fs.existsSync(dotEnvLoc); + const env = dotEnvExists ? dotenv.config({ path: dotEnvLoc }) : null; + + return childProcess.execFileSync( + "./node_modules/.bin/rescript-edgedb", + command, + { + cwd: cwd, + env: { + ...process.env, + ...env?.parsed, + }, + } + ); +} + +function updateContent(e: TextDocument) { + if (e.languageId === "rescript") { + const text = e.getText(); + const cwd = findProjectPackageJsonRoot(e.fileName); + if (cwd != null && text.includes("%edgeql(")) { + const tempFile = createFileInTempDir(); + fs.writeFileSync(tempFile, text); + try { + const dataFromCli = callRescriptEdgeDBCli(["extract", tempFile], cwd); + const data: dataFromFile[] = JSON.parse(dataFromCli.toString()); + edgeqlContent.set(e.fileName, data); + } catch (e) { + console.error(e); + } finally { + fs.rmSync(tempFile); + } + } else { + edgeqlContent.delete(e.fileName); + } + } +} + +async function setupCodeActions(context: ExtensionContext) { + context.subscriptions.push( + workspace.onDidOpenTextDocument((e) => { + updateContent(e); + }) + ); + context.subscriptions.push( + workspace.onDidCloseTextDocument((e) => { + updateContent(e); + }) + ); + context.subscriptions.push( + workspace.onDidChangeTextDocument((e) => { + updateContent(e.document); + }) + ); +} + +export async function activate(context: ExtensionContext) { + let currentWorkspacePath = workspace.workspaceFolders?.[0].uri.fsPath; + if (currentWorkspacePath == null) throw new Error("Init failed."); + + await Promise.all([setupErrorLogWatcher(context), setupCodeActions(context)]); + + context.subscriptions.push( + commands.registerCommand( + "vscode-rescript-edgedb-open-ui", + async (query: string, cwd: string) => { + const url = JSON.parse( + callRescriptEdgeDBCli(["ui-url"], cwd).toString() + ); + const lines = query.trim().split("\n"); + // First line has the comment with the query name, so the second line will have the offset + const offset = lines[1].match(/^\s*/)?.[0].length ?? 0; + const offsetAsStr = Array.from({ length: offset }) + .map((_) => " ") + .join(""); + + await env.clipboard.writeText( + lines + .map((l) => { + const leadingWhitespace = l.slice(0, offset); + if (leadingWhitespace === offsetAsStr) { + return l.slice(offset); + } + + return l; + }) + .join("\n") + ); + window.showInformationMessage( + "The EdgeQL query was copied to the clipboard!\nOpening the EdgeDB UI in the browser..." + ); + env.openExternal(Uri.parse(`${url}/editor`)); + } + ) + ); + + context.subscriptions.push( + languages.registerCodeActionsProvider( + { + language: "rescript", + }, + { + provideCodeActions(document, range, _context, _token) { + const cwd = findProjectPackageJsonRoot(document.fileName); + const contentInFile = edgeqlContent.get(document.fileName) ?? []; + const targetWithCursor = contentInFile.find((c) => { + const start = new Position(c.start.line, c.start.col); + const end = new Position(c.end.line, c.end.col); + return ( + range.start.isAfterOrEqual(start) && + range.end.isBeforeOrEqual(end) + ); + }); + + if (targetWithCursor != null && cwd != null) { + return [ + { + title: "Open the EdgeDB UI query editor", + command: "vscode-rescript-edgedb-open-ui", + arguments: [targetWithCursor.content, cwd], + tooltip: "The EdgeQL query will be copied to the clipboard.", + }, + ]; + } + + return []; + }, + } + ) + ); +} + export function deactivate() {}