diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 10b41046..f17d2aee 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,6 +25,14 @@ jobs: sudo apt-get -y update sudo apt-get -y install --fix-missing xvfb + - uses: quarto-dev/quarto-actions/setup@v2 + + - name: Build vscode extension + run: | + cd apps/vscode + yarn install + yarn run build + - name: Compile and run tests run: | yarn install --immutable --immutable-cache --check-cache diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index bac4465d..72d82e9b 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -29,9 +29,9 @@ import { WorkspaceSymbol } from "vscode-languageserver"; -import { CompletionItem, Hover, Location } from "vscode-languageserver-types" +import { CompletionItem, Hover, Location } from "vscode-languageserver-types"; -import { createConnection } from "vscode-languageserver/node" +import { createConnection } from "vscode-languageserver/node"; import { URI } from "vscode-uri"; import { TextDocument } from "vscode-languageserver-textdocument"; @@ -48,6 +48,8 @@ import { initializeQuarto } from "./quarto"; import { registerDiagnostics } from "./diagnostics"; + + // Create a connection for the server. The connection uses Node's IPC as a transport. // Also include all preview / proposed LSP features. const connection = createConnection(ProposedFeatures.all); @@ -98,7 +100,7 @@ connection.onInitialize((params: InitializeParams) => { } return mdLs?.getCompletionItems(document, params.position, params.context, config, token) || []; - }) + }); connection.onHover(async (params, token): Promise => { logger.logRequest('hover'); @@ -108,7 +110,7 @@ connection.onInitialize((params: InitializeParams) => { return null; } return mdLs?.getHover(document, params.position, config, token); - }) + }); connection.onDocumentLinks(async (params, token): Promise => { @@ -249,7 +251,7 @@ connection.onInitialized(async () => { capabilities!, config, logger - ) + ); // initialize parser const parser = markdownitParser(); @@ -279,7 +281,7 @@ connection.onInitialized(async () => { onRequest(method: string, handler: (params: unknown[]) => Promise) { return connection.onRequest(method, handler); } - } + }; // register custom methods registerCustomMethods(quarto, lspConnection, documents); diff --git a/apps/vscode/.gitignore b/apps/vscode/.gitignore index 48687620..0bdf5abf 100644 --- a/apps/vscode/.gitignore +++ b/apps/vscode/.gitignore @@ -1,3 +1,4 @@ out/ *.vsix test-out/ +examples-out/ diff --git a/apps/vscode/.vscode-test.mjs b/apps/vscode/.vscode-test.mjs index 1487c1d4..638bef89 100644 --- a/apps/vscode/.vscode-test.mjs +++ b/apps/vscode/.vscode-test.mjs @@ -4,5 +4,8 @@ export default defineConfig([ { files: 'test-out/*.test.js', workspaceFolder: 'src/test/examples', + mocha: { + timeout: 3000, + }, }, ]); diff --git a/apps/vscode/src/providers/editor/editor.ts b/apps/vscode/src/providers/editor/editor.ts index 45a7347c..80723103 100644 --- a/apps/vscode/src/providers/editor/editor.ts +++ b/apps/vscode/src/providers/editor/editor.ts @@ -69,6 +69,8 @@ import { import { ExtensionHost } from "../../host"; import { TabInputCustom } from "vscode"; +const kVisualModeConfirmed = "visualModeConfirmed"; + export interface QuartoVisualEditor extends QuartoEditor { hasFocus(): Promise; getActiveBlockContext(): Promise; @@ -87,6 +89,18 @@ export function activateEditor( // return commands return [ + { + id: 'quarto.test_setkVisualModeConfirmedTrue', + execute() { + context.globalState.update(kVisualModeConfirmed, true); + } + }, + { + id: 'quarto.test_isInVisualEditor', + execute() { + return VisualEditorProvider.activeEditor() !== undefined; + } + }, editInVisualModeCommand(), editInSourceModeCommand(), toggleEditModeCommand(), @@ -99,7 +113,7 @@ export class VisualEditorProvider implements CustomTextEditorProvider { // track the last contents of any active untitled docs (used // for recovering from attempt to edit ) - private static activeUntitled?: { uri: Uri, content: string }; + private static activeUntitled?: { uri: Uri, content: string; }; // track the last edited line of code in text editors (used for syncing position) private static editorLastSourcePos = new Map(); @@ -312,7 +326,6 @@ export class VisualEditorProvider implements CustomTextEditorProvider { private readonly lspRequest: JsonRpcRequestTransport, private readonly engine: MarkdownEngine) { } - public async resolveCustomTextEditor( document: TextDocument, webviewPanel: WebviewPanel, @@ -333,7 +346,6 @@ export class VisualEditorProvider implements CustomTextEditorProvider { }; // prompt the user - const kVisualModeConfirmed = "visualModeConfirmed"; // Check for environment variables to force the state of the visual editor confirmation modal // QUARTO_VISUAL_EDITOR_CONFIRMED > PW_TEST > CI diff --git a/apps/vscode/src/test/examples/hello.qmd b/apps/vscode/src/test/examples/hello.qmd index 324c1e2f..c4cdf60a 100644 --- a/apps/vscode/src/test/examples/hello.qmd +++ b/apps/vscode/src/test/examples/hello.qmd @@ -8,3 +8,5 @@ format: html ```{python} 1 + 1 ``` + +*YO!* diff --git a/apps/vscode/src/test/extension.ts b/apps/vscode/src/test/extension.ts new file mode 100644 index 00000000..272b3d25 --- /dev/null +++ b/apps/vscode/src/test/extension.ts @@ -0,0 +1,13 @@ +import * as vscode from "vscode"; + +export const QUARTO_EXTENSION_ID = 'quarto.quarto'; + +export function extension() { + const extension = vscode.extensions.getExtension(QUARTO_EXTENSION_ID); + + if (extension === undefined) { + throw new Error(`Extension ${QUARTO_EXTENSION_ID} not found`); + } + + return extension; +} diff --git a/apps/vscode/src/test/quartoDoc.test.ts b/apps/vscode/src/test/quartoDoc.test.ts index 67569d41..deb7d879 100644 --- a/apps/vscode/src/test/quartoDoc.test.ts +++ b/apps/vscode/src/test/quartoDoc.test.ts @@ -1,13 +1,64 @@ import * as vscode from "vscode"; import * as assert from "assert"; -import { exampleWorkspacePath } from "./test-utils"; +import { exampleWorkspacePath, exampleWorkspaceOutPath, copyFile, wait } from "./test-utils"; import { isQuartoDoc } from "../core/doc"; +import { extension } from "./extension"; -suite("Quarto basics", () => { - test("Can open a Quarto document", async () => { - const doc = await vscode.workspace.openTextDocument(exampleWorkspacePath("hello.qmd")); +const APPROX_TIME_TO_OPEN_VISUAL_EDITOR = 1600; + +suite("Quarto basics", function () { + // Before we run any tests, we should copy any files that get edited in the tests to file under `exampleWorkspaceOutPath` + suiteSetup(async function () { + const didCopyFile = await copyFile(exampleWorkspacePath('hello.qmd'), exampleWorkspaceOutPath('hello.qmd')); + assert.ok(didCopyFile); + }); + + test("Can open a Quarto document", async function () { + const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd")); const editor = await vscode.window.showTextDocument(doc); + assert.strictEqual(editor?.document.languageId, "quarto"); assert.strictEqual(isQuartoDoc(editor?.document), true); }); + + // Note: the following tests may be flaky. They rely on waiting estimated amounts of time for commands to complete. + test("Can edit in visual mode", async function () { + // don't run this in CI for now because we haven't figured out how to get the LSP to start + if (process.env['CI']) this.skip(); + + const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd")); + const editor = await vscode.window.showTextDocument(doc); + + // manually confirm visual mode so dialogue pop-up doesn't show because dialogues cause test errors + // and switch to visual editor + await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue"); + await wait(300); // It seems necessary to wait around 300ms for this command to be done. + await vscode.commands.executeCommand("quarto.editInVisualMode"); + await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR); + + assert.ok(await vscode.commands.executeCommand("quarto.test_isInVisualEditor")); + }); + // Note: this test runs after the previous test, so `hello.qmd` has already been touched by the previous + // test. That's okay for this test, but could cause issues if you expect a qmd to look how it + // does in `/examples`. + test("Roundtrip doesn't change hello.qmd", async function () { + // don't run this in CI for now because we haven't figured out how to get the LSP to start + if (process.env['CI']) this.skip(); + + const doc = await vscode.workspace.openTextDocument(exampleWorkspaceOutPath("hello.qmd")); + const editor = await vscode.window.showTextDocument(doc); + + const docTextBefore = doc.getText(); + + // switch to visual editor and back + await vscode.commands.executeCommand("quarto.test_setkVisualModeConfirmedTrue"); + await wait(300); + await vscode.commands.executeCommand("quarto.editInVisualMode"); + await wait(APPROX_TIME_TO_OPEN_VISUAL_EDITOR); + await vscode.commands.executeCommand("quarto.editInSourceMode"); + await wait(300); + + const docTextAfter = doc.getText(); + assert.ok(docTextBefore === docTextAfter); + }); }); diff --git a/apps/vscode/src/test/test-utils.ts b/apps/vscode/src/test/test-utils.ts index fe0007d0..f395c704 100644 --- a/apps/vscode/src/test/test-utils.ts +++ b/apps/vscode/src/test/test-utils.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import * as vscode from "vscode"; /** @@ -16,3 +17,32 @@ export const WORKSPACE_PATH = path.join(TEST_PATH, "examples"); export function exampleWorkspacePath(file: string): string { return path.join(WORKSPACE_PATH, file); } +export function exampleWorkspaceOutPath(file: string): string { + return path.join(WORKSPACE_PATH, 'examples-out', file); +} + +export function wait(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export async function copyFile( + sourcePath: string, + destPath: string, +): Promise { + try { + const wsedit = new vscode.WorkspaceEdit(); + const data = await vscode.workspace.fs.readFile( + vscode.Uri.file(sourcePath) + ); + const destFileUri = vscode.Uri.file(destPath); + wsedit.createFile(destFileUri, { ignoreIfExists: true }); + + await vscode.workspace.fs.writeFile(destFileUri, data); + + let isDone = await vscode.workspace.applyEdit(wsedit); + if (isDone) return true; + else return false; + } catch (err) { + return false; + } +}