Skip to content

Commit a27784e

Browse files
authored
Always use a unique virtual document file path (#688)
* Add an `outputChannel` we can use for logging It is shared with the LSP Client * Rework `virtualDocUriFromTempFile()` to ensure we always use unique vdoc file names * CHANGELOG
1 parent 8ee12b5 commit a27784e

File tree

4 files changed

+98
-44
lines changed

4 files changed

+98
-44
lines changed

apps/vscode/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## 1.120.0 (unreleased)
44

5+
- Fix issue where format on save could overwrite the contents of a document with incorrect results (<https://github.com/quarto-dev/quarto/pull/688>).
6+
57
## 1.119.0 (Release on 2025-03-21)
68

79
- Use `QUARTO_VISUAL_EDITOR_CONFIRMED` > `PW_TEST` > `CI` to bypass (`true`) or force (`false`) the Visual Editor confirmation dialogue (<https://github.com/quarto-dev/quarto/pull/654>).

apps/vscode/src/lsp/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Location,
2323
LocationLink,
2424
Definition,
25+
LogOutputChannel,
2526
} from "vscode";
2627
import {
2728
LanguageClient,
@@ -70,7 +71,8 @@ let client: LanguageClient;
7071
export async function activateLsp(
7172
context: ExtensionContext,
7273
quartoContext: QuartoContext,
73-
engine: MarkdownEngine
74+
engine: MarkdownEngine,
75+
outputChannel: LogOutputChannel
7476
) {
7577

7678
// The server is implemented in node
@@ -132,6 +134,7 @@ export async function activateLsp(
132134
},
133135
],
134136
middleware,
137+
outputChannel
135138
};
136139

137140
// Create the language client and start the client.

apps/vscode/src/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import { configuredQuartoPath } from "./core/quarto";
3737
import { activateDenoConfig } from "./providers/deno-config";
3838

3939
export async function activate(context: vscode.ExtensionContext) {
40+
// create output channel for extension logs and lsp client logs
41+
const outputChannel = vscode.window.createOutputChannel("Quarto", { log: true });
42+
43+
outputChannel.info("Activating Quarto extension.");
4044

4145
// create extension host
4246
const host = extensionHost();
@@ -84,7 +88,7 @@ export async function activate(context: vscode.ExtensionContext) {
8488
activateDenoConfig(context, engine);
8589

8690
// lsp
87-
const lspClient = await activateLsp(context, quartoContext, engine);
91+
const lspClient = await activateLsp(context, quartoContext, engine, outputChannel);
8892

8993
// provide visual editor
9094
const editorCommands = activateEditor(context, host, quartoContext, lspClient, engine);
@@ -125,6 +129,8 @@ export async function activate(context: vscode.ExtensionContext) {
125129

126130
// activate providers common to browser/node
127131
activateCommon(context, host, engine, commands);
132+
133+
outputChannel.info("Activated Quarto extension.");
128134
}
129135

130136
export async function deactivate() {

apps/vscode/src/vdoc/vdoc-tempfile.ts

Lines changed: 85 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,45 +29,50 @@ import {
2929
} from "vscode";
3030
import { VirtualDoc, VirtualDocUri } from "./vdoc";
3131

32+
/**
33+
* Create an on disk temporary file containing the contents of the virtual document
34+
*
35+
* @param virtualDoc The document to use when populating the temporary file
36+
* @param docPath The path to the original document the virtual document is
37+
* based on. When `local` is `true`, this is used to determine the directory
38+
* to create the temporary file in.
39+
* @param local Whether or not the temporary file should be created "locally" in
40+
* the workspace next to `docPath` or in a temporary directory outside the
41+
* workspace.
42+
* @returns A `VirtualDocUri`
43+
*/
3244
export async function virtualDocUriFromTempFile(
3345
virtualDoc: VirtualDoc,
3446
docPath: string,
3547
local: boolean
3648
): Promise<VirtualDocUri> {
37-
const newVirtualDocUri = (doc: TextDocument) =>
38-
<VirtualDocUri>{
39-
uri: doc.uri,
40-
cleanup: async () => await deleteDocument(doc),
41-
};
42-
43-
// if this is local then create it alongside the docPath and return a cleanup
44-
// function to remove it when the action is completed.
45-
if (local || virtualDoc.language.localTempFile) {
46-
const ext = virtualDoc.language.extension;
47-
const vdocPath = path.join(path.dirname(docPath), `.vdoc.${ext}`);
48-
fs.writeFileSync(vdocPath, virtualDoc.content);
49-
const vdocUri = Uri.file(vdocPath);
50-
const doc = await workspace.openTextDocument(vdocUri);
51-
return newVirtualDocUri(doc);
52-
}
49+
const useLocal = local || virtualDoc.language.localTempFile;
5350

54-
// write the virtual doc as a temp file
55-
const vdocTempFile = createVirtualDocTempFile(virtualDoc);
51+
// If `useLocal`, then create the temporary document alongside the `docPath`
52+
// so tools like formatters have access to workspace configuration. Otherwise,
53+
// create it in a temp directory.
54+
const virtualDocFilepath = useLocal
55+
? createVirtualDocLocalFile(virtualDoc, path.dirname(docPath))
56+
: createVirtualDocTempfile(virtualDoc);
5657

57-
// open the document and save a reference to it
58-
const vdocUri = Uri.file(vdocTempFile);
59-
const doc = await workspace.openTextDocument(vdocUri);
58+
const virtualDocUri = Uri.file(virtualDocFilepath);
59+
const virtualDocTextDocument = await workspace.openTextDocument(virtualDocUri);
6060

61-
// TODO: Reevaluate whether this is necessary. Old comment:
62-
// > if this is the first time getting a virtual doc for this
63-
// > language then execute a dummy request to cause it to load
64-
await commands.executeCommand<Hover[]>(
65-
"vscode.executeHoverProvider",
66-
vdocUri,
67-
new Position(0, 0)
68-
);
61+
if (!useLocal) {
62+
// TODO: Reevaluate whether this is necessary. Old comment:
63+
// > if this is the first time getting a virtual doc for this
64+
// > language then execute a dummy request to cause it to load
65+
await commands.executeCommand<Hover[]>(
66+
"vscode.executeHoverProvider",
67+
virtualDocUri,
68+
new Position(0, 0)
69+
);
70+
}
6971

70-
return newVirtualDocUri(doc);
72+
return <VirtualDocUri>{
73+
uri: virtualDocTextDocument.uri,
74+
cleanup: async () => await deleteDocument(virtualDocTextDocument),
75+
};
7176
}
7277

7378
// delete a document
@@ -82,19 +87,57 @@ async function deleteDocument(doc: TextDocument) {
8287
}
8388
}
8489

85-
// create temp files for vdocs. use a base directory that has a subdirectory
86-
// for each extension used within the document. this is a no-op if the
87-
// file already exists
8890
tmp.setGracefulCleanup();
89-
const vdocTempDir = tmp.dirSync().name;
90-
function createVirtualDocTempFile(virtualDoc: VirtualDoc) {
91-
const ext = virtualDoc.language.extension;
92-
const dir = path.join(vdocTempDir, ext);
93-
if (!fs.existsSync(dir)) {
94-
fs.mkdirSync(dir);
91+
const VIRTUAL_DOC_TEMP_DIRECTORY = tmp.dirSync().name;
92+
93+
/**
94+
* Creates a virtual document in a temporary directory
95+
*
96+
* The temporary directory is automatically cleaned up on process exit.
97+
*
98+
* @param virtualDoc The document to use when populating the temporary file
99+
* @returns The path to the temporary file
100+
*/
101+
function createVirtualDocTempfile(virtualDoc: VirtualDoc): string {
102+
const filepath = generateVirtualDocFilepath(VIRTUAL_DOC_TEMP_DIRECTORY, virtualDoc.language.extension);
103+
createVirtualDoc(filepath, virtualDoc.content);
104+
return filepath;
105+
}
106+
107+
/**
108+
* Creates a virtual document in the provided directory
109+
*
110+
* @param virtualDoc The document to use when populating the temporary file
111+
* @param directory The directory to create the temporary file in
112+
* @returns The path to the temporary file
113+
*/
114+
function createVirtualDocLocalFile(virtualDoc: VirtualDoc, directory: string): string {
115+
const filepath = generateVirtualDocFilepath(directory, virtualDoc.language.extension);
116+
createVirtualDoc(filepath, virtualDoc.content);
117+
return filepath;
118+
}
119+
120+
/**
121+
* Creates a file filled with the provided content
122+
*/
123+
function createVirtualDoc(filepath: string, content: string): void {
124+
const directory = path.dirname(filepath);
125+
126+
if (!fs.existsSync(directory)) {
127+
fs.mkdirSync(directory);
95128
}
96-
const tmpPath = path.join(vdocTempDir, ext, ".intellisense." + uuid.v4() + "." + ext);
97-
fs.writeFileSync(tmpPath, virtualDoc.content);
98129

99-
return tmpPath;
130+
fs.writeFileSync(filepath, content);
131+
}
132+
133+
/**
134+
* Generates a unique virtual document file path
135+
*
136+
* It is important for virtual documents to have unique file paths. If a static
137+
* name like `.vdoc.{ext}` is used, it is possible for one language server
138+
* request to overwrite the contents of the virtual document while another
139+
* language server request is running (#683).
140+
*/
141+
function generateVirtualDocFilepath(directory: string, extension: string): string {
142+
return path.join(directory, ".vdoc." + uuid.v4() + "." + extension);
100143
}

0 commit comments

Comments
 (0)