From a643afd021adb8fc1c5d4af2cde88f92dfe09398 Mon Sep 17 00:00:00 2001 From: ikappaki Date: Sun, 15 Sep 2024 11:19:53 +0100 Subject: [PATCH] Preserve whitespaces format of evaluation error in tooltip --- CHANGELOG.md | 2 + .../integration/suite/annotations-test.ts | 74 ++++++++++++++++++- src/providers/annotations.ts | 58 ++++++++++++--- 3 files changed, 123 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d120d96..27cfea1d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Calva. ## [Unreleased] +- [Preserve whitespaces format of evaluation error in tooltip](https://github.com/BetterThanTomorrow/calva/issues/2623) + ## [2.0.471] - 2024-09-12 - [Trim leading newlines from the result/error string before inline display](https://github.com/BetterThanTomorrow/calva/issues/2617) diff --git a/src/extension-test/integration/suite/annotations-test.ts b/src/extension-test/integration/suite/annotations-test.ts index ca2b23eb8..b4a7f376f 100644 --- a/src/extension-test/integration/suite/annotations-test.ts +++ b/src/extension-test/integration/suite/annotations-test.ts @@ -24,8 +24,6 @@ suite('Annotations suite', () => { }); test('decorate result trims leading whitespaces', async function () { - testUtil.log(suite, 'activeEditor'); - // any file would do const testFilePath = path.join(testUtil.testDataDir, 'test.clj'); const editor = await testUtil.openFile(testFilePath); @@ -63,4 +61,76 @@ suite('Annotations suite', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); testUtil.log(suite, 'test.clj closed'); }); + + test('tooltip contents of evaluated result', async function () { + // any file would do + const testFilePath = path.join(testUtil.testDataDir, 'test.clj'); + const editor = await testUtil.openFile(testFilePath); + + // Any range will do, the range below happens to be around "hover-map". + const selection = new vscode.Selection(2, 5, 2, 14); + editor.selection = selection; + const proxyEditor = testUtil.createVscTextEditorProxy(editor); + + // will eventually call on the editor's `setDecorations` fn with the text to render. + const setDecorationsSpy = sinon.spy(proxyEditor, 'setDecorations'); + + const resultString = ' :Line1 with a leading space\n:Line2 some text\n\nLine4 the end.'; + + // SUCCESS case, format should contain the result in a clojure code block + { + annotations.decorateSelection( + resultString, + selection, + proxyEditor, + null, + annotations.AnnotationStatus.SUCCESS + ); + + //await testUtil.sleep(60000); + + assert.deepStrictEqual( + setDecorationsSpy.firstCall.args[0], + annotations._getEvalSelectionDecorationTypes(annotations.AnnotationStatus.SUCCESS) + ); + assert.deepStrictEqual(setDecorationsSpy.firstCall.args[1], []); + assert.deepStrictEqual( + setDecorationsSpy.secondCall.args[0], + annotations._getEvalSelectionDecorationTypes(annotations.AnnotationStatus.SUCCESS) + ); + const decorateOpts = setDecorationsSpy.secondCall.args[1] as vscode.DecorationOptions[]; + const hoverMessage = decorateOpts[0].hoverMessage as vscode.MarkdownString; + const expectedResultSuccess = `${annotations._getDecorateSelectionHeader( + resultString + )}\n\`\`\`clojure\n${resultString}\n\`\`\``; + assert.strictEqual(hoverMessage.value, expectedResultSuccess); + } + + sinon.reset(); + + // ERROR case, should contain the result in a plain code block (```) + { + annotations.decorateSelection( + resultString, + selection, + proxyEditor, + null, + annotations.AnnotationStatus.ERROR + ); + + assert.deepStrictEqual( + setDecorationsSpy.firstCall.args[0], + annotations._getEvalSelectionDecorationTypes(annotations.AnnotationStatus.ERROR) + ); + assert.deepStrictEqual(setDecorationsSpy.firstCall.args[1], []); + const decorateOpts = setDecorationsSpy.secondCall.args[1] as vscode.DecorationOptions[]; + const hoverMessage = decorateOpts[0].hoverMessage as vscode.MarkdownString; + const expectedResultSuccess = `${annotations._getDecorateSelectionHeader( + resultString + )}\n\`\`\`\n${resultString}\n\`\`\``; + assert.strictEqual(hoverMessage.value, expectedResultSuccess); + } + + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); }); diff --git a/src/providers/annotations.ts b/src/providers/annotations.ts index 4b10ef5e0..8e4dd4ede 100644 --- a/src/providers/annotations.ts +++ b/src/providers/annotations.ts @@ -63,7 +63,7 @@ function setResultDecorations(editor: vscode.TextEditor, ranges) { editor.setDecorations(evalResultsDecorationType, ranges); } -function setSelectionDecorations(editor, ranges, status) { +function setSelectionDecorations(editor: vscode.TextEditor, ranges, status) { const key = editor.document.uri + ':selectionDecorationRanges:' + status; util.cljsLib.setStateValue(key, ranges); editor.setDecorations(evalSelectionDecorationTypes[status], ranges); @@ -93,6 +93,8 @@ function clearAllEvaluationDecorations() { void vscode.commands.executeCommand('setContext', 'calva:hasInlineResults', false); } +// Amongst other things, this function removes any leading whitespace +// from the RESULTSTRING displayed in the decoration. function decorateResults( resultString, hasError, @@ -114,6 +116,32 @@ function decorateResults( void vscode.commands.executeCommand('setContext', 'calva:hasInlineResults', true); } +// Returns a string of commands seperated by `|` to display in the +// results tooltip for the given RESULTSTRING. +// +// The commands are +// +// 1. Copy results to the clipboard. +// 2. Reveal the output destination. +function getDecorateSelectionCmdsString(resultString: string) { + const copyCommandUri = `command:calva.copyAnnotationHoverText?${encodeURIComponent( + JSON.stringify([{ text: resultString }]) + )}`, + copyCommandMd = `[Copy](${copyCommandUri} "Copy results to the clipboard")`; + const openWindowCommandUri = `command:calva.showResultOutputDestination`, + openWindowCommandMd = `[Show Output](${openWindowCommandUri} "Reveal the output destination")`; + + return `${copyCommandMd} | ${openWindowCommandMd}`; +} + +// Amongst other things, this function generates the hover content for +// RESULTSTRING based on the STATUS value: +// +// - `ERROR`: Includes the commands header from `getDecorateSelectionCmdsString`, followed by RESULTSTRING +// wrapped in a Markdown plain code block preserving whitespaces. +// +// - `SUCCESS`: Includes the commands header from `getDecorateSelectionCmdsString`, followed by RESULTSTRING +// wrapped in a Markdown Clojure code block. function decorateSelection( resultString: string, codeSelection: vscode.Selection, @@ -130,17 +158,15 @@ function decorateSelection( }); decoration['range'] = codeSelection; if (status != AnnotationStatus.PENDING && status != AnnotationStatus.REPL_WINDOW) { - const copyCommandUri = `command:calva.copyAnnotationHoverText?${encodeURIComponent( - JSON.stringify([{ text: resultString }]) - )}`, - copyCommandMd = `[Copy](${copyCommandUri} "Copy results to the clipboard")`; - const openWindowCommandUri = `command:calva.showResultOutputDestination`, - openWindowCommandMd = `[Show Output](${openWindowCommandUri} "Reveal the output destination")`; + const codeBlockLang = status == AnnotationStatus.ERROR ? '' : 'clojure'; const hoverMessage = new vscode.MarkdownString( - `${copyCommandMd} | ${openWindowCommandMd}\n` + '```clojure\n' + resultString + '\n```' + getDecorateSelectionCmdsString(resultString) + + `\n\`\`\`${codeBlockLang}\n` + + resultString + + '\n```' ); hoverMessage.isTrusted = true; - decoration['hoverMessage'] = status == AnnotationStatus.ERROR ? resultString : hoverMessage; + decoration['hoverMessage'] = hoverMessage; } // for (let s = 0; s < evalSelectionDecorationTypes.length; s++) { // setSelectionDecorations(editor, [], s);. @@ -166,7 +192,21 @@ function onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { function copyHoverTextCommand(args: { [x: string]: string }) { void vscode.env.clipboard.writeText(args['text']); } + +// ------------- EXPORT FOR UNIT TEST USE ONLY ---------------------------------- + +const _getDecorateSelectionHeader = getDecorateSelectionCmdsString; + +/// retuns the selection decoration type of the given annotation STATUS. +function _getEvalSelectionDecorationTypes(status: AnnotationStatus) { + return evalSelectionDecorationTypes[status]; +} + +// ------------------------------------------------------------------------------ + export default { + _getDecorateSelectionHeader, + _getEvalSelectionDecorationTypes, AnnotationStatus, clearEvaluationDecorations, clearAllEvaluationDecorations,