import * as vscode from "vscode"; import { getRightViewColumn, getTestFile } from "./util/vscode"; import * as path from "path"; import { getLanguageLibrary } from "./languages"; export function showAnswerInTextEditor( filename: string, range: vscode.Range, answer: string ) { vscode.workspace.openTextDocument(vscode.Uri.file(filename)).then((doc) => { const editor = vscode.window.activeTextEditor; if (!editor) { return; } // Open file, reveal range, show decoration vscode.window.showTextDocument(doc).then((new_editor) => { new_editor.revealRange( new vscode.Range(range.end, range.end), vscode.TextEditorRevealType.InCenter ); let decorationType = vscode.window.createTextEditorDecorationType({ after: { contentText: answer + "\n", color: "rgb(0, 255, 0, 0.8)", }, backgroundColor: "rgb(0, 255, 0, 0.2)", }); new_editor.setDecorations(decorationType, [range]); vscode.window.showInformationMessage("Answer found!"); // Remove decoration when user moves cursor vscode.window.onDidChangeTextEditorSelection((e) => { if ( e.textEditor === new_editor && e.selections[0].active.line !== range.end.line ) { new_editor.setDecorations(decorationType, []); } }); }); }); } type DecorationKey = { editorUri: string; options: vscode.DecorationOptions; decorationType: vscode.TextEditorDecorationType; }; class DecorationManager { private editorToDecorations = new Map< string, Map >(); constructor() { vscode.window.onDidChangeVisibleTextEditors((editors) => { for (const editor of editors) { if (editor.document.isClosed) { this.editorToDecorations.delete(editor.document.uri.toString()); } } }); } private rerenderDecorations( editorUri: string, decorationType: vscode.TextEditorDecorationType ) { const editor = vscode.window.activeTextEditor; if (!editor) { return; } const decorationTypes = this.editorToDecorations.get(editorUri); if (!decorationTypes) { return; } const decorations = decorationTypes.get(decorationType); if (!decorations) { return; } editor.setDecorations(decorationType, decorations); } addDecoration(key: DecorationKey) { let decorationTypes = this.editorToDecorations.get(key.editorUri); if (!decorationTypes) { decorationTypes = new Map(); decorationTypes.set(key.decorationType, [key.options]); this.editorToDecorations.set(key.editorUri, decorationTypes); } else { const decorations = decorationTypes.get(key.decorationType); if (!decorations) { decorationTypes.set(key.decorationType, [key.options]); } else { decorations.push(key.options); } } this.rerenderDecorations(key.editorUri, key.decorationType); vscode.window.onDidChangeTextEditorSelection((event) => { if (event.textEditor.document.fileName === key.editorUri) { this.deleteAllDecorations(key.editorUri); } }); } deleteDecoration(key: DecorationKey) { let decorationTypes = this.editorToDecorations.get(key.editorUri); if (!decorationTypes) { return; } let decorations = decorationTypes?.get(key.decorationType); if (!decorations) { return; } decorations = decorations.filter((decOpts) => decOpts !== key.options); decorationTypes.set(key.decorationType, decorations); this.rerenderDecorations(key.editorUri, key.decorationType); } deleteAllDecorations(editorUri: string) { let decorationTypes = this.editorToDecorations.get(editorUri)?.keys(); if (!decorationTypes) { return; } this.editorToDecorations.delete(editorUri); for (let decorationType of decorationTypes) { this.rerenderDecorations(editorUri, decorationType); } } } export const decorationManager = new DecorationManager(); function constructBaseKey( editor: vscode.TextEditor, lineno: number, decorationType?: vscode.TextEditorDecorationType ): DecorationKey { return { editorUri: editor.document.uri.toString(), options: { range: new vscode.Range(lineno, 0, lineno, 0), }, decorationType: decorationType || vscode.window.createTextEditorDecorationType({}), }; } const gutterSpinnerDecorationType = vscode.window.createTextEditorDecorationType({ gutterIconPath: vscode.Uri.file( path.join(__dirname, "..", "media", "spinner.gif") ), gutterIconSize: "contain", }); export function showGutterSpinner( editor: vscode.TextEditor, lineno: number ): DecorationKey { const key = constructBaseKey(editor, lineno, gutterSpinnerDecorationType); decorationManager.addDecoration(key); return key; } export function showLintMessage( editor: vscode.TextEditor, lineno: number, msg: string ): DecorationKey { const key = constructBaseKey(editor, lineno); key.decorationType = vscode.window.createTextEditorDecorationType({ after: { contentText: "Linting error", color: "rgb(255, 0, 0, 0.6)", }, gutterIconPath: vscode.Uri.file( path.join(__dirname, "..", "media", "error.png") ), gutterIconSize: "contain", }); key.options.hoverMessage = msg; decorationManager.addDecoration(key); return key; } export function highlightCode( editor: vscode.TextEditor, range: vscode.Range, removeOnClick: boolean = true ): DecorationKey { const decorationType = vscode.window.createTextEditorDecorationType({ backgroundColor: "rgb(255, 255, 0, 0.1)", }); const key = { editorUri: editor.document.uri.toString(), options: { range, }, decorationType, }; decorationManager.addDecoration(key); if (removeOnClick) { vscode.window.onDidChangeTextEditorSelection((e) => { if (e.textEditor === editor) { decorationManager.deleteDecoration(key); } }); } return key; } // Show unit test const pythonImportDistinguisher = (line: string): boolean => { if (line.startsWith("from") || line.startsWith("import")) { return true; } return false; }; const javascriptImportDistinguisher = (line: string): boolean => { if (line.startsWith("import")) { return true; } return false; }; const importDistinguishersMap: { [fileExtension: string]: (line: string) => boolean; } = { js: javascriptImportDistinguisher, ts: javascriptImportDistinguisher, py: pythonImportDistinguisher, }; function getImportsFromFileString( fileString: string, importDistinguisher: (line: string) => boolean ): Set { let importLines = new Set(); for (let line of fileString.split("\n")) { if (importDistinguisher(line)) { importLines.add(line); } } return importLines; } function removeRedundantLinesFrom( fileContents: string, linesToRemove: Set ): string { let fileLines = fileContents.split("\n"); fileLines = fileLines.filter((line: string) => { return !linesToRemove.has(line); }); return fileLines.join("\n"); } export async function writeAndShowUnitTest( filename: string, test: string ): Promise { return new Promise((resolve, reject) => { let testFilename = getTestFile(filename, true); vscode.workspace.openTextDocument(testFilename).then((doc) => { let fileContent = doc.getText(); let fileEmpty = fileContent.trim() === ""; let existingImportLines = getImportsFromFileString( fileContent, importDistinguishersMap[doc.fileName.split(".").at(-1) || ".py"] ); // Remove redundant imports, make sure pytest is there test = removeRedundantLinesFrom(test, existingImportLines); test = (fileEmpty ? `${getLanguageLibrary(".py").writeImport( testFilename, filename )}\nimport pytest\n\n` : "\n\n") + test.trim() + "\n"; vscode.window .showTextDocument(doc, getRightViewColumn()) .then((editor) => { let lastLine = editor.document.lineAt(editor.document.lineCount - 1); let testRange = new vscode.Range( lastLine.range.end, new vscode.Position( test.split("\n").length + lastLine.range.end.line, 0 ) ); editor .edit((edit) => { edit.insert(lastLine.range.end, test); return true; }) .then((success) => { if (!success) reject("Failed to insert test"); let key = highlightCode(editor, testRange); resolve(key); }); }); }); }); }