From 06e5c0d64e66d8632a617b7aefeed3a93d4bf301 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 2 Apr 2025 16:32:18 -0400 Subject: [PATCH 1/3] Add an `outputChannel` we can use for logging It is shared with the LSP Client --- apps/vscode/src/lsp/client.ts | 5 ++++- apps/vscode/src/main.ts | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index d7615ce6..8d0897f5 100644 --- a/apps/vscode/src/lsp/client.ts +++ b/apps/vscode/src/lsp/client.ts @@ -22,6 +22,7 @@ import { Location, LocationLink, Definition, + LogOutputChannel, } from "vscode"; import { LanguageClient, @@ -70,7 +71,8 @@ let client: LanguageClient; export async function activateLsp( context: ExtensionContext, quartoContext: QuartoContext, - engine: MarkdownEngine + engine: MarkdownEngine, + outputChannel: LogOutputChannel ) { // The server is implemented in node @@ -132,6 +134,7 @@ export async function activateLsp( }, ], middleware, + outputChannel }; // Create the language client and start the client. diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 51239b44..160a149c 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -37,6 +37,10 @@ import { configuredQuartoPath } from "./core/quarto"; import { activateDenoConfig } from "./providers/deno-config"; export async function activate(context: vscode.ExtensionContext) { + // create output channel for extension logs and lsp client logs + const outputChannel = vscode.window.createOutputChannel("Quarto", { log: true }); + + outputChannel.info("Activating Quarto extension."); // create extension host const host = extensionHost(); @@ -84,7 +88,7 @@ export async function activate(context: vscode.ExtensionContext) { activateDenoConfig(context, engine); // lsp - const lspClient = await activateLsp(context, quartoContext, engine); + const lspClient = await activateLsp(context, quartoContext, engine, outputChannel); // provide visual editor const editorCommands = activateEditor(context, host, quartoContext, lspClient, engine); @@ -125,6 +129,8 @@ export async function activate(context: vscode.ExtensionContext) { // activate providers common to browser/node activateCommon(context, host, engine, commands); + + outputChannel.info("Activated Quarto extension."); } export async function deactivate() { From 11a9c1048b26656c130b9345c6c2f03c2251e019 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 2 Apr 2025 17:34:34 -0400 Subject: [PATCH 2/3] Rework `virtualDocUriFromTempFile()` to ensure we always use unique vdoc file names --- apps/vscode/src/vdoc/vdoc-tempfile.ts | 127 +++++++++++++++++--------- 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/apps/vscode/src/vdoc/vdoc-tempfile.ts b/apps/vscode/src/vdoc/vdoc-tempfile.ts index 3b4851d9..699df311 100644 --- a/apps/vscode/src/vdoc/vdoc-tempfile.ts +++ b/apps/vscode/src/vdoc/vdoc-tempfile.ts @@ -29,45 +29,50 @@ import { } from "vscode"; import { VirtualDoc, VirtualDocUri } from "./vdoc"; +/** + * Create an on disk temporary file containing the contents of the virtual document + * + * @param virtualDoc The document to use when populating the temporary file + * @param docPath The path to the original document the virtual document is + * based on. When `local` is `true`, this is used to determine the directory + * to create the temporary file in. + * @param local Whether or not the temporary file should be created "locally" in + * the workspace next to `docPath` or in a temporary directory outside the + * workspace. + * @returns A `VirtualDocUri` + */ export async function virtualDocUriFromTempFile( virtualDoc: VirtualDoc, docPath: string, local: boolean ): Promise { - const newVirtualDocUri = (doc: TextDocument) => - { - uri: doc.uri, - cleanup: async () => await deleteDocument(doc), - }; - - // if this is local then create it alongside the docPath and return a cleanup - // function to remove it when the action is completed. - if (local || virtualDoc.language.localTempFile) { - const ext = virtualDoc.language.extension; - const vdocPath = path.join(path.dirname(docPath), `.vdoc.${ext}`); - fs.writeFileSync(vdocPath, virtualDoc.content); - const vdocUri = Uri.file(vdocPath); - const doc = await workspace.openTextDocument(vdocUri); - return newVirtualDocUri(doc); - } + const useLocal = local || virtualDoc.language.localTempFile; - // write the virtual doc as a temp file - const vdocTempFile = createVirtualDocTempFile(virtualDoc); + // If `useLocal`, then create the temporary document alongside the `docPath` + // so tools like formatters have access to workspace configuration. Otherwise, + // create it in a temp directory. + const virtualDocFilepath = useLocal + ? createVirtualDocLocalFile(virtualDoc, path.dirname(docPath)) + : createVirtualDocTempfile(virtualDoc); - // open the document and save a reference to it - const vdocUri = Uri.file(vdocTempFile); - const doc = await workspace.openTextDocument(vdocUri); + const virtualDocUri = Uri.file(virtualDocFilepath); + const virtualDocTextDocument = await workspace.openTextDocument(virtualDocUri); - // TODO: Reevaluate whether this is necessary. Old comment: - // > if this is the first time getting a virtual doc for this - // > language then execute a dummy request to cause it to load - await commands.executeCommand( - "vscode.executeHoverProvider", - vdocUri, - new Position(0, 0) - ); + if (!useLocal) { + // TODO: Reevaluate whether this is necessary. Old comment: + // > if this is the first time getting a virtual doc for this + // > language then execute a dummy request to cause it to load + await commands.executeCommand( + "vscode.executeHoverProvider", + virtualDocUri, + new Position(0, 0) + ); + } - return newVirtualDocUri(doc); + return { + uri: virtualDocTextDocument.uri, + cleanup: async () => await deleteDocument(virtualDocTextDocument), + }; } // delete a document @@ -82,19 +87,57 @@ async function deleteDocument(doc: TextDocument) { } } -// create temp files for vdocs. use a base directory that has a subdirectory -// for each extension used within the document. this is a no-op if the -// file already exists tmp.setGracefulCleanup(); -const vdocTempDir = tmp.dirSync().name; -function createVirtualDocTempFile(virtualDoc: VirtualDoc) { - const ext = virtualDoc.language.extension; - const dir = path.join(vdocTempDir, ext); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); +const VIRTUAL_DOC_TEMP_DIRECTORY = tmp.dirSync().name; + +/** + * Creates a virtual document in a temporary directory + * + * The temporary directory is automatically cleaned up on process exit. + * + * @param virtualDoc The document to use when populating the temporary file + * @returns The path to the temporary file + */ +function createVirtualDocTempfile(virtualDoc: VirtualDoc): string { + const filepath = generateVirtualDocFilepath(VIRTUAL_DOC_TEMP_DIRECTORY, virtualDoc.language.extension); + createVirtualDoc(filepath, virtualDoc.content); + return filepath; +} + +/** + * Creates a virtual document in the provided directory + * + * @param virtualDoc The document to use when populating the temporary file + * @param directory The directory to create the temporary file in + * @returns The path to the temporary file + */ +function createVirtualDocLocalFile(virtualDoc: VirtualDoc, directory: string): string { + const filepath = generateVirtualDocFilepath(directory, virtualDoc.language.extension); + createVirtualDoc(filepath, virtualDoc.content); + return filepath; +} + +/** + * Creates a file filled with the provided content + */ +function createVirtualDoc(filepath: string, content: string): void { + const directory = path.dirname(filepath); + + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory); } - const tmpPath = path.join(vdocTempDir, ext, ".intellisense." + uuid.v4() + "." + ext); - fs.writeFileSync(tmpPath, virtualDoc.content); - return tmpPath; + fs.writeFileSync(filepath, content); +} + +/** + * Generates a unique virtual document file path + * + * It is important for virtual documents to have unique file paths. If a static + * name like `.vdoc.{ext}` is used, it is possible for one language server + * request to overwrite the contents of the virtual document while another + * language server request is running (#683). + */ +function generateVirtualDocFilepath(directory: string, extension: string): string { + return path.join(directory, ".vdoc." + uuid.v4() + "." + extension); } From 9f58463c439c1d0a6d67cbef07cf07cc4b41ce92 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 2 Apr 2025 17:39:54 -0400 Subject: [PATCH 3/3] CHANGELOG --- apps/vscode/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 3232b0b7..e163a06a 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -2,6 +2,8 @@ ## 1.120.0 (unreleased) +- Fix issue where format on save could overwrite the contents of a document with incorrect results (). + ## 1.119.0 (Release on 2025-03-21) - Use `QUARTO_VISUAL_EDITOR_CONFIRMED` > `PW_TEST` > `CI` to bypass (`true`) or force (`false`) the Visual Editor confirmation dialogue ().