diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de8edf52..d48d60832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes to Calva. ## [Unreleased] +## [2.0.255] - 2022-03-18 +- Maintenance: [Update more TypeScript code to be compatible with strictNullChecks.](https://github.com/BetterThanTomorrow/calva/pull/1581) + ## [2.0.254] - 2022-03-16 - [Add commands for starting and stopping clojure-lsp](https://github.com/BetterThanTomorrow/calva/pull/1592) - Maintenance: [Dumb down the token cursor some dealing with meta data and readers](https://github.com/BetterThanTomorrow/calva/pull/1585) diff --git a/package-lock.json b/package-lock.json index f0000ecd3..a0a7bb3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "calva", - "version": "2.0.254", + "version": "2.0.255", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "calva", - "version": "2.0.254", + "version": "2.0.255", "license": "MIT", "dependencies": { "@types/escape-html": "0.0.20", diff --git a/package.json b/package.json index 00f3f8750..db44d655c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Calva: Clojure & ClojureScript Interactive Programming", "description": "Integrated REPL, formatter, Paredit, and more. Powered by cider-nrepl and clojure-lsp.", "icon": "assets/calva.png", - "version": "2.0.254", + "version": "2.0.255", "publisher": "betterthantomorrow", "author": { "name": "Better Than Tomorrow", diff --git a/src/analytics.ts b/src/analytics.ts index 5ca7b9996..cb29d2ab0 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -45,7 +45,7 @@ export default class Analytics { ); } - private userID(): string { + private userID(): string | undefined { const KEY = 'userLogID'; if (this.store.get(KEY) == undefined) { const newID = uuid.uuid(); diff --git a/src/calva-fmt/src/format.ts b/src/calva-fmt/src/format.ts index 346e519a8..b42b69380 100644 --- a/src/calva-fmt/src/format.ts +++ b/src/calva-fmt/src/format.ts @@ -5,7 +5,7 @@ import { getIndent, getDocumentOffset, MirroredDocument, - mustGetDocument, + getDocument, } from '../../doc-mirror/index'; import { formatTextAtRange, @@ -21,10 +21,10 @@ export async function indentPosition( position: vscode.Position, document: vscode.TextDocument ) { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const pos = new vscode.Position(position.line, 0); const indent = getIndent( - mustGetDocument(document).model.lineInputModel, + getDocument(document).model.lineInputModel, getDocumentOffset(document, position), await config.getConfig() ); @@ -54,9 +54,9 @@ export async function indentPosition( export async function formatRangeEdits( document: vscode.TextDocument, range: vscode.Range -): Promise { +): Promise { const text: string = document.getText(range); - const mirroredDoc: MirroredDocument = mustGetDocument(document); + const mirroredDoc: MirroredDocument = getDocument(document); const startIndex = document.offsetAt(range.start); const endIndex = document.offsetAt(range.end); const cursor = mirroredDoc.getTokenCursor(startIndex); @@ -91,7 +91,7 @@ export async function formatPositionInfo( const doc: vscode.TextDocument = editor.document; const pos: vscode.Position = editor.selection.active; const index = doc.offsetAt(pos); - const mirroredDoc: MirroredDocument = mustGetDocument(doc); + const mirroredDoc: MirroredDocument = getDocument(doc); const cursor = mirroredDoc.getTokenCursor(index); const formatDepth = extraConfig['format-depth'] ? extraConfig['format-depth'] @@ -240,7 +240,7 @@ async function _formatRange( allText: string, range: number[], eol: string -): Promise { +): Promise { const d = { 'range-text': rangeText, 'all-text': allText, diff --git a/src/calva-fmt/src/providers/ontype_formatter.ts b/src/calva-fmt/src/providers/ontype_formatter.ts index ba22fc94f..d1e074234 100644 --- a/src/calva-fmt/src/providers/ontype_formatter.ts +++ b/src/calva-fmt/src/providers/ontype_formatter.ts @@ -14,7 +14,7 @@ export class FormatOnTypeEditProvider _position: vscode.Position, ch: string, _options - ): Promise { + ): Promise { let keyMap = vscode.workspace .getConfiguration() .get('calva.paredit.defaultKeyMap'); @@ -24,21 +24,20 @@ export class FormatOnTypeEditProvider keyMap === 'strict' && getConfig().strictPreventUnmatchedClosingBracket ) { - const mDoc: EditableDocument = - docMirror.mustGetDocument(document); + const mDoc: EditableDocument = docMirror.getDocument(document); const tokenCursor = mDoc.getTokenCursor(); if (tokenCursor.withinComment()) { - return null; + return undefined; } return paredit.backspace(mDoc).then((fulfilled) => { paredit.close(mDoc, ch); - return null; + return undefined; }); } else { - return null; + return undefined; } } - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const pos = editor.selection.active; if ( @@ -61,6 +60,6 @@ export class FormatOnTypeEditProvider } } - return null; + return undefined; } } diff --git a/src/clojuredocs.ts b/src/clojuredocs.ts index 69a19aa05..333a7812a 100644 --- a/src/clojuredocs.ts +++ b/src/clojuredocs.ts @@ -63,7 +63,7 @@ export function printTextToRichCommentCommand(args: { [x: string]: string }) { function printTextToRichComment(text: string, position?: number) { const doc = util.getDocument({}); - const mirrorDoc = docMirror.mustGetDocument(doc); + const mirrorDoc = docMirror.getDocument(doc); paredit.addRichComment( mirrorDoc, position ? position : mirrorDoc.selection.active, @@ -74,10 +74,10 @@ function printTextToRichComment(text: string, position?: number) { export async function getExamplesHover( document: vscode.TextDocument, position: vscode.Position -): Promise { +): Promise { const docs = await clojureDocsLookup(document, position); if (!docs) { - return null; + return undefined; } return getHoverForDocs( docs, @@ -183,7 +183,7 @@ async function clojureDocsLookup( p?: vscode.Position ): Promise { const doc = d ? d : util.getDocument({}); - const position = p ? p : util.mustGetActiveTextEditor().selection.active; + const position = p ? p : util.getActiveTextEditor().selection.active; const symbol = util.getWordAtPosition(doc, position); const ns = namespace.getNamespace(doc); const session = replSession.getSession(util.getFileType(doc)); @@ -237,7 +237,7 @@ function rawDocs2DocsEntry( docsResult: any, symbol: string, ns: string -): DocsEntry { +): DocsEntry | undefined { const docs = docsResult.clojuredocs; if (docs) { return { @@ -262,6 +262,6 @@ function rawDocs2DocsEntry( }; } else { // console.log(`No results for ${ns}/${symbol} from ${docsResult.fromServer}`); - return null; + return undefined; } } diff --git a/src/connector.ts b/src/connector.ts index ff8fd40e8..2626b1ce2 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -238,7 +238,7 @@ type connectFn = ( session: NReplSession, name: string, checkSuccess: checkConnectedFn -) => Promise; +) => Promise; async function evalConnectCode( newCljsSession: NReplSession, @@ -249,8 +249,8 @@ async function evalConnectCode( errorProcessors: processOutputFn[] = [] ): Promise { const chan = state.connectionLogChannel(); - const err = [], - out = [], + const err: string[] = [], + out: string[] = [], result = newCljsSession.eval(code, 'user', { stdout: (x) => { out.push(util.stripAnsi(x)); @@ -290,7 +290,9 @@ export interface ReplType { let translatedReplType: ReplType; -async function figwheelOrShadowBuilds(cljsTypeName: string): Promise { +async function figwheelOrShadowBuilds( + cljsTypeName: string +): Promise { if (cljsTypeName.includes('Figwheel Main')) { return await getFigwheelMainBuilds(); } else if (cljsTypeName.includes('shadow-cljs')) { @@ -298,7 +300,7 @@ async function figwheelOrShadowBuilds(cljsTypeName: string): Promise { } } -function updateInitCode(build: string, initCode): string { +function updateInitCode(build: string, initCode): string | undefined { if (build && typeof initCode === 'object') { if (['node-repl', 'browser-repl'].includes(build)) { return initCode.repl.replace('%REPL%', build); @@ -308,7 +310,7 @@ function updateInitCode(build: string, initCode): string { } else if (build && typeof initCode === 'string') { return initCode.replace('%BUILD%', `"${build}"`); } - return null; + return undefined; } function createCLJSReplType( @@ -836,9 +838,7 @@ export default { } setStateValue('cljc', newSession); if ( - outputWindow.isResultsDoc( - util.mustGetActiveTextEditor().document - ) + outputWindow.isResultsDoc(util.getActiveTextEditor().document) ) { outputWindow.setSession(newSession, undefined); replSession.updateReplSessionType(); diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 83ac5f3a8..71127f5d5 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,6 +1,7 @@ import { Scanner, Token, ScannerState } from './clojure-lexer'; import { LispTokenCursor } from './token-cursor'; import { deepEqual as equal } from '../util/object'; +import { isUndefined } from 'lodash'; let scanner: Scanner; @@ -291,7 +292,7 @@ export class LineInputModel implements EditableModel { const st = this.getRowCol(Math.min(start, end)); const en = this.getRowCol(Math.max(start, end)); - const lines = []; + const lines: string[] = []; if (st[0] == en[0]) { lines[0] = this.lines[st[0]].text.substring(st[1], en[1]); } else { @@ -581,6 +582,8 @@ export class LineInputModel implements EditableModel { lastIndex = i; } return new LispTokenCursor(this, row, line.tokens.length - 1); + } else { + throw new Error('Unable to get token cursor for LineInputModel!'); } } } @@ -609,6 +612,10 @@ export class StringDocument implements EditableDocument { selectionStack: ModelEditSelection[] = []; getTokenCursor(offset?: number, previous?: boolean): LispTokenCursor { + if (isUndefined(offset)) { + throw new Error('Expected a cursor for StringDocument!'); + } + return this.model.getTokenCursor(offset); } diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index fa01b38e3..889c85ba4 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -807,10 +807,10 @@ export function backspace( start: number = doc.selection.anchor, end: number = doc.selection.active ): Thenable { - const cursor = doc.getTokenCursor(start); if (start != end) { return doc.backspace(); } else { + const cursor = doc.getTokenCursor(start); const nextToken = cursor.getToken(); const p = start; const prevToken = @@ -857,10 +857,10 @@ export function deleteForward( start: number = doc.selectionLeft, end: number = doc.selectionRight ) { - const cursor = doc.getTokenCursor(start); if (start != end) { void doc.delete(); } else { + const cursor = doc.getTokenCursor(start); const prevToken = cursor.getPrevToken(); const nextToken = cursor.getToken(); const p = start; diff --git a/src/custom-snippets.ts b/src/custom-snippets.ts index 62fc47469..7af83d708 100644 --- a/src/custom-snippets.ts +++ b/src/custom-snippets.ts @@ -15,7 +15,7 @@ async function evaluateCustomCodeSnippetCommand(codeOrKey?: string) { } async function evaluateCodeOrKey(codeOrKey?: string) { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const currentLine = editor.selection.active.line; const currentColumn = editor.selection.active.character; const currentFilename = editor.document.fileName; diff --git a/src/debugger/calva-debug.ts b/src/debugger/calva-debug.ts index c7beced36..d74a6680b 100644 --- a/src/debugger/calva-debug.ts +++ b/src/debugger/calva-debug.ts @@ -287,7 +287,7 @@ class CalvaDebugSession extends LoggingDebugSession { new Position(positionLine, positionColumn) ); const tokenCursor = docMirror - .mustGetDocument(document) + .getDocument(document) .getTokenCursor(offset); try { diff --git a/src/debugger/decorations.ts b/src/debugger/decorations.ts index e1cc7f057..7f6fee061 100644 --- a/src/debugger/decorations.ts +++ b/src/debugger/decorations.ts @@ -144,7 +144,7 @@ function triggerUpdateAndRenderDecorations() { timeout = undefined; } if (enabled) { - const editor = util.getActiveTextEditor(); + const editor = util.tryToGetActiveTextEditor(); if (editor) { timeout = setTimeout(() => { const cljSession = replSession.getSession('clj'); @@ -166,7 +166,7 @@ function activate() { }); vscode.workspace.onDidChangeTextDocument((event) => { - const activeEditor = util.getActiveTextEditor(); + const activeEditor = util.tryToGetActiveTextEditor(); if ( activeEditor && event.document === activeEditor.document && diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index ffe3bc0a9..4d4f7774e 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -29,7 +29,7 @@ export class DocumentModel implements EditableModel { modelEdits: ModelEdit[], options: ModelEditOptions ): Thenable { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), undoStopBefore = !!options.undoStopBefore; return editor .edit( @@ -85,7 +85,7 @@ export class DocumentModel implements EditableModel { oldSelection?: [number, number], newSelection?: [number, number] ) { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), document = editor.document; builder.insert(document.positionAt(offset), text); } @@ -98,7 +98,7 @@ export class DocumentModel implements EditableModel { oldSelection?: [number, number], newSelection?: [number, number] ) { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), document = editor.document, range = new vscode.Range( document.positionAt(start), @@ -114,7 +114,7 @@ export class DocumentModel implements EditableModel { oldSelection?: [number, number], newSelection?: [number, number] ) { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), document = editor.document, range = new vscode.Range( document.positionAt(offset), @@ -135,7 +135,7 @@ export class DocumentModel implements EditableModel { return this.lineInputModel.getOffsetForLine(line); } - public getTokenCursor(offset: number, previous: boolean = false) { + public getTokenCursor(offset: number, previous?: boolean) { return this.lineInputModel.getTokenCursor(offset, previous); } } @@ -144,13 +144,13 @@ export class MirroredDocument implements EditableDocument { get selectionLeft(): number { return this.document.offsetAt( - utilities.mustGetActiveTextEditor().selection.anchor + utilities.getActiveTextEditor().selection.anchor ); } get selectionRight(): number { return this.document.offsetAt( - utilities.mustGetActiveTextEditor().selection.active + utilities.getActiveTextEditor().selection.active ); } @@ -166,7 +166,7 @@ export class MirroredDocument implements EditableDocument { } public insertString(text: string) { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), selection = editor.selection, wsEdit = new vscode.WorkspaceEdit(), edit = vscode.TextEdit.insert( @@ -180,7 +180,7 @@ export class MirroredDocument implements EditableDocument { } set selection(selection: ModelEditSelection) { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), document = editor.document, anchor = document.positionAt(selection.anchor), active = document.positionAt(selection.active); @@ -193,7 +193,7 @@ export class MirroredDocument implements EditableDocument { } public getSelectionText() { - const editor = utilities.mustGetActiveTextEditor(), + const editor = utilities.getActiveTextEditor(), selection = editor.selection; return this.document.getText(selection); } @@ -238,12 +238,12 @@ function processChanges(event: vscode.TextDocumentChangeEvent) { model.lineInputModel.deletedLines.clear(); } -export function getDocument(doc: vscode.TextDocument) { +export function tryToGetDocument(doc: vscode.TextDocument) { return documents.get(doc); } -export function mustGetDocument(doc: vscode.TextDocument) { - const mirrorDoc = documents.get(doc); +export function getDocument(doc: vscode.TextDocument) { + const mirrorDoc = tryToGetDocument(doc); if (isUndefined(mirrorDoc)) { throw new Error('Missing mirror document!'); @@ -256,11 +256,11 @@ export function getDocumentOffset( doc: vscode.TextDocument, position: vscode.Position ) { - const model = mustGetDocument(doc).model; + const model = getDocument(doc).model; return model.getOffsetForLine(position.line) + position.character; } -function addDocument(doc: vscode.TextDocument): boolean { +function addDocument(doc?: vscode.TextDocument): boolean { if (doc && doc.languageId == 'clojure') { if (!documents.has(doc)) { const document = new MirroredDocument(doc); @@ -281,7 +281,7 @@ export function activate() { } registered = true; - addDocument(utilities.getDocument({})); + addDocument(utilities.tryToGetDocument({})); vscode.workspace.onDidCloseTextDocument((e) => { if (e.languageId == 'clojure') { diff --git a/src/edit.ts b/src/edit.ts index 0599a71b1..27ce93b42 100644 --- a/src/edit.ts +++ b/src/edit.ts @@ -5,11 +5,11 @@ import * as docMirror from './doc-mirror/index'; // Relies on that `when` claus guards this from being called // when the cursor is before the comment marker export function continueCommentCommand() { - const document = util.getDocument({}); + const document = util.tryToGetDocument({}); if (document && document.languageId === 'clojure') { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const position = editor.selection.active; - const cursor = docMirror.mustGetDocument(document).getTokenCursor(); + const cursor = docMirror.getDocument(document).getTokenCursor(); if (cursor.getToken().type !== 'comment') { if (cursor.getPrevToken().type === 'comment') { cursor.previous(); diff --git a/src/evaluate.ts b/src/evaluate.ts index 605262974..5e84b12c7 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -91,7 +91,7 @@ async function evaluateCode( const filePath = options.filePath; const session: NReplSession = options.session; const ns = options.ns; - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); let result = null; if (code.length > 0) { @@ -253,19 +253,19 @@ async function evaluateCode( } async function evaluateSelection(document = {}, options) { - const doc = util.getDocument(document); const selectionFn: ( editor: vscode.TextEditor ) => [vscode.Selection, string] = options.selectionFn; if (getStateValue('connected')) { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); state.analytics().logEvent('Evaluation', 'selectionFn').send(); const selection = selectionFn(editor); const codeSelection: vscode.Selection = selection[0]; let code = selection[1]; [codeSelection, code]; + const doc = util.getDocument(document); const ns = namespace.getNamespace(doc); const line = codeSelection.start.line; const column = codeSelection.start.character; @@ -471,8 +471,11 @@ function evaluateStartOfFileToCursor(document = {}, options = {}) { ); } -async function loadFile(document, pprintOptions: PrettyPrintingOptions) { - const doc = util.getDocument(document); +async function loadFile( + document: vscode.TextDocument | Record | undefined, + pprintOptions: PrettyPrintingOptions +) { + const doc = util.tryToGetDocument(document); const fileType = util.getFileType(doc); const ns = namespace.getNamespace(doc); const session = replSession.getSession(util.getFileType(doc)); @@ -524,7 +527,7 @@ async function loadFile(document, pprintOptions: PrettyPrintingOptions) { } async function evaluateUser(code: string) { - const fileType = util.getFileType(util.getDocument({})), + const fileType = util.getFileType(util.tryToGetDocument({})), session = replSession.getSession(fileType); if (session) { try { @@ -543,20 +546,20 @@ async function evaluateUser(code: string) { async function requireREPLUtilitiesCommand() { if (util.getConnectedState()) { const chan = state.outputChannel(), - ns = namespace.getDocumentNamespace(util.getDocument({})), + ns = namespace.getDocumentNamespace(util.tryToGetDocument({})), CLJS_FORM = "(use '[cljs.repl :only [apropos dir doc find-doc print-doc pst source]])", CLJ_FORM = '(clojure.core/apply clojure.core/require clojure.main/repl-requires)', sessionType = replSession.getReplSessionTypeFromState(), form = sessionType == 'cljs' ? CLJS_FORM : CLJ_FORM, - fileType = util.getFileType(util.getDocument({})), + fileType = util.getFileType(util.tryToGetDocument({})), session = replSession.getSession(fileType); if (session) { try { await namespace.createNamespaceFromDocumentIfNotExists( - util.getDocument({}) + util.tryToGetDocument({}) ); await session.eval("(in-ns '" + ns + ')', session.client.ns) .value; @@ -580,7 +583,7 @@ async function requireREPLUtilitiesCommand() { async function copyLastResultCommand() { const chan = state.outputChannel(); const session = replSession.getSession( - util.getFileType(util.getDocument({})) + util.getFileType(util.tryToGetDocument({})) ); const value = await session.eval('*1', session.client.ns).value; diff --git a/src/extension-test/integration/suite/extension-test.ts b/src/extension-test/integration/suite/extension-test.ts index dd7fb84ff..c52fe1a0b 100644 --- a/src/extension-test/integration/suite/extension-test.ts +++ b/src/extension-test/integration/suite/extension-test.ts @@ -12,7 +12,7 @@ import * as vscode from 'vscode'; // import * as myExtension from '../extension'; import * as outputWindow from '../../../results-output/results-doc'; import { commands } from 'vscode'; -import { mustGetDocument } from '../../../doc-mirror'; +import { getDocument } from '../../../doc-mirror'; void vscode.window.showInformationMessage('Tests running. Yay!'); @@ -85,7 +85,7 @@ suite('Extension Test Suite', () => { await sleep(500); // wait a little longer for repl output to be done console.log('connected to repl'); - const resultsDoc = mustGetDocument(await outputWindow.openResultsDoc()); + const resultsDoc = getDocument(await outputWindow.openResultsDoc()); // focus the clojure file await vscode.workspace.openTextDocument(testUri).then((doc) => diff --git a/src/extension-test/unit/test-runner-test.ts b/src/extension-test/unit/test-runner-test.ts index 76059af0f..3454ff176 100644 --- a/src/extension-test/unit/test-runner-test.ts +++ b/src/extension-test/unit/test-runner-test.ts @@ -123,7 +123,7 @@ describe('test result processing', () => { var: 'test', message: '', }) - ).toBe(null); + ).toBeUndefined(); expect( cider.detailedMessage({ diff --git a/src/extension-test/unit/util/string-test.ts b/src/extension-test/unit/util/string-test.ts index 7e4008345..231c50899 100644 --- a/src/extension-test/unit/util/string-test.ts +++ b/src/extension-test/unit/util/string-test.ts @@ -41,10 +41,10 @@ describe('string', () => { }); describe('getTextAfterLastOccurrenceOfSubstring', () => { - it('returns null if substring does not exist', () => { - expect(null).toBe( + it('returns undefined if substring does not exist', () => { + expect( getTextAfterLastOccurrenceOfSubstring('hello world', '123') - ); + ).toBeUndefined(); }); it('returns text after last occurrence of substring', () => { expect('foo').toBe( diff --git a/src/file-switcher/file-switcher.ts b/src/file-switcher/file-switcher.ts index 8aa32dbf4..4d3b450b8 100644 --- a/src/file-switcher/file-switcher.ts +++ b/src/file-switcher/file-switcher.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as util from './util'; import * as projectRoot from '../project-root'; -import { mustGetActiveTextEditor } from '../utilities'; +import { getActiveTextEditor } from '../utilities'; function openFile(file) { return vscode.workspace @@ -40,7 +40,7 @@ function askToCreateANewFile(dir, file) { } export async function toggleBetweenImplAndTest() { - const activeFile = mustGetActiveTextEditor(); + const activeFile = getActiveTextEditor(); const openedFilename = activeFile.document.fileName; const projectRootUri = await projectRoot.getProjectRootUri(); diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index ffe5ad24c..0171bf6a8 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -5,7 +5,7 @@ import { isArray } from 'util'; import * as docMirror from '../../doc-mirror/index'; import { Token, validPair } from '../../cursor-doc/clojure-lexer'; import { LispTokenCursor } from '../../cursor-doc/token-cursor'; -import { getActiveTextEditor, mustGetActiveTextEditor } from '../../utilities'; +import { tryToGetActiveTextEditor, getActiveTextEditor } from '../../utilities'; type StackItem = { char: string; @@ -265,7 +265,7 @@ function updateRainbowBrackets() { } const doc = activeEditor.document, - mirrorDoc = docMirror.mustGetDocument(doc), + mirrorDoc = docMirror.getDocument(doc), rainbow = rainbowTypes.map(() => []), rainbowGuides = rainbowTypes.map(() => []), misplaced = [], @@ -498,7 +498,7 @@ function matchPairs() { return; } - const matches = []; + const matches: { range: vscode.Range }[] = []; activeEditor.selections.forEach((selection) => { const match_before = matchBefore(selection), match_after = matchAfter(selection); @@ -541,7 +541,7 @@ function decorateGuide( function decorateActiveGuides() { const activeGuides = []; - activeEditor = mustGetActiveTextEditor(); + activeEditor = getActiveTextEditor(); if (activeGuidesTypes) { activeGuidesTypes.forEach((type) => activeEditor.setDecorations(type, []) @@ -549,7 +549,7 @@ function decorateActiveGuides() { } activeEditor.selections.forEach((selection) => { const doc = activeEditor.document; - const mirrorDoc = docMirror.mustGetDocument(doc); + const mirrorDoc = docMirror.getDocument(doc); const cursor = mirrorDoc.getTokenCursor(doc.offsetAt(selection.start)); const visitedEndPositions = [selection.start]; findActiveGuide: while (cursor.forwardList() && cursor.upList()) { @@ -586,7 +586,7 @@ function decorateActiveGuides() { } export function activate(context: vscode.ExtensionContext) { - activeEditor = mustGetActiveTextEditor(); + activeEditor = getActiveTextEditor(); vscode.window.onDidChangeActiveTextEditor( (editor) => { @@ -602,7 +602,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.window.onDidChangeTextEditorSelection( (event) => { if ( - event.textEditor === getActiveTextEditor() && + event.textEditor === tryToGetActiveTextEditor() && is_clojure(event.textEditor) ) { if (lastHighlightedEditor !== event.textEditor) { diff --git a/src/lsp/main.ts b/src/lsp/main.ts index 074e7c88d..24afec32c 100644 --- a/src/lsp/main.ts +++ b/src/lsp/main.ts @@ -300,8 +300,8 @@ function registerLspCommand(command: ClojureLspCommand): vscode.Disposable { (m) => m.substring(1).toUpperCase() )}`; return vscode.commands.registerCommand(vscodeCommand, async () => { - const editor = util.mustGetActiveTextEditor(); - const document = util.getDocument(editor.document); + const editor = util.getActiveTextEditor(); + const document = util.tryToGetDocument(editor.document); if (document && document.languageId === 'clojure') { const line = editor.selection.start.line; const column = editor.selection.start.character; @@ -321,7 +321,7 @@ function registerLspCommand(command: ClojureLspCommand): vscode.Disposable { } async function codeLensReferencesHandler(_, line, character): Promise { - util.mustGetActiveTextEditor().selection = new vscode.Selection( + util.getActiveTextEditor().selection = new vscode.Selection( line - 1, character - 1, line - 1, @@ -333,7 +333,7 @@ async function codeLensReferencesHandler(_, line, character): Promise { } function resolveMacroAsCommandHandler(): void { - const activeTextEditor = util.getActiveTextEditor(); + const activeTextEditor = util.tryToGetActiveTextEditor(); if (activeTextEditor?.document?.languageId === 'clojure') { const documentUri = decodeURIComponent( activeTextEditor.document.uri.toString() diff --git a/src/namespace.ts b/src/namespace.ts index 121c53a4d..c813d3b69 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -8,18 +8,23 @@ import * as utilities from './utilities'; import * as replSession from './nrepl/repl-session'; import { NReplSession } from './nrepl'; -export function getNamespace(doc: vscode.TextDocument) { +export function getNamespace(doc?: vscode.TextDocument) { if (outputWindow.isResultsDoc(doc)) { - return outputWindow.getNs(); + const outputWindowNs = outputWindow.getNs(); + utilities.assertIsDefined( + outputWindowNs, + 'Expected output window to have a namespace!' + ); + return outputWindowNs; } let ns = 'user'; if (doc && doc.languageId == 'clojure') { try { const cursor: LispTokenCursor = docMirror - .mustGetDocument(doc) + .getDocument(doc) .getTokenCursor(0); cursor.forwardWhitespace(true); - let token: Token = null, + let token: Token | undefined = undefined, foundNsToken: boolean = false, foundNsId: boolean = false; do { @@ -72,7 +77,7 @@ export function getNamespace(doc: vscode.TextDocument) { export async function createNamespaceFromDocumentIfNotExists(doc) { if (utilities.getConnectedState()) { - const document = utilities.getDocument(doc); + const document = utilities.tryToGetDocument(doc); if (document) { const ns = getNamespace(document); const client = replSession.getSession( @@ -90,7 +95,7 @@ export async function createNamespaceFromDocumentIfNotExists(doc) { } export function getDocumentNamespace(document = {}) { - const doc = utilities.getDocument(document); + const doc = utilities.tryToGetDocument(document); return getNamespace(doc); } diff --git a/src/nrepl/cider.ts b/src/nrepl/cider.ts index dfa0d0dc2..9f2e80d74 100644 --- a/src/nrepl/cider.ts +++ b/src/nrepl/cider.ts @@ -64,7 +64,7 @@ function stripTrailingNewlines(s: string): string { } function resultMessage(resultItem: Readonly): string { - const msg = []; + const msg: string[] = []; if (resultItem.context && resultItem.context !== 'false') { msg.push(resultItem.context); } @@ -89,7 +89,7 @@ export function cleanUpWhiteSpace(result: TestResult) { // ; 6 tests finished, all passing 👍, ns: 1, vars: 2 // ; 6 tests finished, problems found. 😭 errors: 0, failures: 1, ns: 1, vars: 2 export function summaryMessage(summary: Readonly): string { - const msg = []; + const msg: string[] = []; if (summary.test > 0) { msg.push(summary.test + ' tests finished'); @@ -155,8 +155,8 @@ export function lineInformation(result: TestResult): string { // If the test passed, return the empty string. // The message contains "comment" lines that are prepended with ; // and "data" lines that should be printed verbatim into the REPL. -export function detailedMessage(result: TestResult): string { - const messages = []; +export function detailedMessage(result: TestResult): string | undefined { + const messages: string[] = []; const message = resultMessage(result); const location = lineInformation(result); @@ -183,7 +183,7 @@ export function detailedMessage(result: TestResult): string { messages.push(`; actual:\n${result.actual}`); } } - return messages.length > 0 ? messages.join('\n') : null; + return messages.length > 0 ? messages.join('\n') : undefined; } // Return a short message that can be shown to user as a Diagnostic. @@ -214,6 +214,12 @@ export function shortMessage(result: TestResult): string { } } -export function hasLineNumber(result: TestResult): boolean { +type TestResultWithLineNumber = Omit & { + line: NonNullable; +}; + +export const hasLineNumber = ( + result: TestResult | TestResultWithLineNumber +): result is TestResultWithLineNumber => { return typeof result.line === 'number'; -} +}; diff --git a/src/nrepl/project-types.ts b/src/nrepl/project-types.ts index 61509182b..bd9f37623 100644 --- a/src/nrepl/project-types.ts +++ b/src/nrepl/project-types.ts @@ -542,7 +542,7 @@ async function cljCommandLine( ? menuSelections.cljAliases : undefined; let aliases: string[] = []; - const aliasesWithMain = []; + const aliasesWithMain: string[] = []; if (launchAliases) { aliases = launchAliases.map(keywordize); } else { diff --git a/src/nrepl/repl-session.ts b/src/nrepl/repl-session.ts index 1bfe04f84..2deccddc3 100644 --- a/src/nrepl/repl-session.ts +++ b/src/nrepl/repl-session.ts @@ -1,11 +1,12 @@ import { NReplSession } from '.'; -import { cljsLib, getDocument, getFileType } from '../utilities'; +import { cljsLib, tryToGetDocument, getFileType } from '../utilities'; import * as outputWindow from '../results-output/results-doc'; +import { isUndefined } from 'lodash'; -function getSession(fileType = undefined): NReplSession { - const doc = getDocument({}); +function getSession(fileType?: string): NReplSession { + const doc = tryToGetDocument({}); - if (fileType === undefined) { + if (isUndefined(fileType)) { fileType = getFileType(doc); } if (fileType.match(/^clj[sc]?/)) { @@ -19,10 +20,10 @@ function getSession(fileType = undefined): NReplSession { } } -function getReplSessionType(connected: boolean): string { - const doc = getDocument({}); +function getReplSessionType(connected: boolean): string | undefined { + const doc = tryToGetDocument({}); const fileType = getFileType(doc); - let sessionType: string = null; + let sessionType: string | undefined = undefined; if (connected) { if (outputWindow.isResultsDoc(doc)) { diff --git a/src/nrepl/repl-start.ts b/src/nrepl/repl-start.ts index 9efffacc9..812960010 100644 --- a/src/nrepl/repl-start.ts +++ b/src/nrepl/repl-start.ts @@ -107,7 +107,7 @@ async function openStoredDoc( storageUri: vscode.Uri, tempDirUri: vscode.Uri, dramFile: DramFile -): Promise<[vscode.TextDocument, vscode.TextEditor]> { +): Promise<[vscode.TextDocument, vscode.TextEditor] | undefined> { const sourceUri = vscode.Uri.file( path.join(storageUri.fsPath, dramFile.path) ); diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index 2c7ec9def..90723947d 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -13,6 +13,7 @@ import { import * as paredit from '../cursor-doc/paredit'; import * as docMirror from '../doc-mirror/index'; import { EditableDocument } from '../cursor-doc/model'; +import { assertIsDefined } from '../utilities'; const onPareditKeyMapChangedEmitter = new EventEmitter(); @@ -364,10 +365,16 @@ const pareditCommands: PareditCommand[] = [ function wrapPareditCommand(command: PareditCommand) { return () => { try { - const textEditor = window.activeTextEditor, - mDoc: EditableDocument = docMirror.mustGetDocument( - textEditor.document - ); + const textEditor = window.activeTextEditor; + + assertIsDefined( + textEditor, + 'Expected window to have an activeTextEditor!' + ); + + const mDoc: EditableDocument = docMirror.getDocument( + textEditor.document + ); if (!enabled || !languages.has(textEditor.document.languageId)) { return; } diff --git a/src/printer.ts b/src/printer.ts index e75ef4c47..f792c49dc 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -1,4 +1,5 @@ import { getConfig } from './config'; +import { assertIsDefined } from './utilities'; export type PrintFnOptions = { name: string; @@ -11,7 +12,7 @@ export type PrettyPrintingOptions = { enabled: boolean; printFn?: PrintFnOptions; printEngine?: 'calva' | 'pprint' | 'fipp' | 'puget' | 'zprint' | 'custom'; - width: number; + width?: number; maxLength?: number; maxDepth?: number; }; @@ -27,9 +28,9 @@ export const disabledPrettyPrinter: PrettyPrintingOptions = { function getPrinter( pprintOptions: PrettyPrintingOptions, printerFn: string, - widthSlug: string, - lengthSlug: string, - depthsSlug: string, + widthSlug?: string, + lengthSlug?: string, + depthsSlug?: string, moreOptions = {} ) { const PRINTER_FN = 'nrepl.middleware.print/print', @@ -107,7 +108,7 @@ export function getServerSidePrinter(pprintOptions: PrettyPrintingOptions) { } } -export function prettyPrintingOptions(): PrettyPrintingOptions { +export function prettyPrintingOptions(): PrettyPrintingOptions | undefined { return getConfig().prettyPrintingOptions; } @@ -116,7 +117,10 @@ export const zprintDependencies = { }; export function getServerSidePrinterDependencies() { - if (prettyPrintingOptions().printEngine === 'zprint') { + const options = prettyPrintingOptions(); + assertIsDefined(options, 'Expected prettyPrintingOptions to be defined!'); + + if (options.printEngine === 'zprint') { return zprintDependencies; } else { return {}; diff --git a/src/project-root.ts b/src/project-root.ts index 517951515..02801647e 100644 --- a/src/project-root.ts +++ b/src/project-root.ts @@ -3,8 +3,8 @@ import * as util from './utilities'; // TODO - this module has some code common with `state`. We can refactor `state` to use this functions. -export function getProjectWsFolder(): vscode.WorkspaceFolder { - const doc = util.getDocument({}); +export function getProjectWsFolder(): vscode.WorkspaceFolder | undefined { + const doc = util.tryToGetDocument({}); if (doc) { const folder = vscode.workspace.getWorkspaceFolder(doc.uri); if (folder) { @@ -22,12 +22,12 @@ export function getProjectWsFolder(): vscode.WorkspaceFolder { export async function findProjectRootUri( projectFileNames: string[], - doc: vscode.TextDocument, - workspaceFolder: vscode.WorkspaceFolder -): Promise { + doc: vscode.TextDocument | undefined, + workspaceFolder: vscode.WorkspaceFolder | undefined +): Promise { let searchUri = doc?.uri || workspaceFolder?.uri; if (searchUri && !(searchUri.scheme === 'untitled')) { - let prev: vscode.Uri; + let prev: vscode.Uri | undefined = undefined; while (searchUri != prev) { try { for (const projectFile of projectFileNames) { @@ -57,7 +57,7 @@ export async function getProjectRootUri(): Promise { 'shadow-cljs.edn', 'deps.edn', ]; - const doc = util.getDocument({}); + const doc = util.tryToGetDocument({}); const workspaceFolder = getProjectWsFolder(); return findProjectRootUri(projectFileNames, doc, workspaceFolder); } diff --git a/src/providers/annotations.ts b/src/providers/annotations.ts index 7b6eda148..b0db35a48 100644 --- a/src/providers/annotations.ts +++ b/src/providers/annotations.ts @@ -29,7 +29,7 @@ const evalResultsDecorationType = vscode.window.createTextEditorDecorationType({ let resultsLocations: [vscode.Range, vscode.Position, vscode.Location][] = []; -function getResultsLocation(pos: vscode.Position): vscode.Location { +function getResultsLocation(pos: vscode.Position): vscode.Location | undefined { for (const [range, _evaluatePosition, location] of resultsLocations) { if (range.contains(pos)) { return location; @@ -37,7 +37,9 @@ function getResultsLocation(pos: vscode.Position): vscode.Location { } } -function getEvaluationPosition(pos: vscode.Position): vscode.Position { +function getEvaluationPosition( + pos: vscode.Position +): vscode.Position | undefined { for (const [range, evaluatePosition, _location] of resultsLocations) { if (range.contains(pos)) { return evaluatePosition; @@ -95,7 +97,7 @@ function setSelectionDecorations(editor, ranges, status) { } function clearEvaluationDecorations(editor?: vscode.TextEditor) { - editor = editor || util.getActiveTextEditor(); + editor = editor || util.tryToGetActiveTextEditor(); if (editor) { util.cljsLib.removeStateValue( editor.document.uri + ':resultDecorationRanges' @@ -204,7 +206,8 @@ function decorateSelection( function onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { if (event.contentChanges.length) { - const activeTextEditor: vscode.TextEditor = util.getActiveTextEditor(); + const activeTextEditor: vscode.TextEditor | undefined = + util.tryToGetActiveTextEditor(); if (activeTextEditor) { const activeDocument = activeTextEditor.document, changeDocument = event.document; diff --git a/src/providers/completion.ts b/src/providers/completion.ts index 896d3c584..5ed895e78 100644 --- a/src/providers/completion.ts +++ b/src/providers/completion.ts @@ -42,17 +42,26 @@ export async function provideCompletionItems( if (util.getConnectedState()) { const toplevelSelection = select.getFormSelection( - document, - position, - true - ), - toplevel = document.getText(toplevelSelection), + document, + position, + true + ); + + util.assertIsDefined( + toplevelSelection, + 'Expected a topLevelSelection!' + ); + + const toplevel = document.getText(toplevelSelection), toplevelStartOffset = document.offsetAt(toplevelSelection.start), toplevelStartCursor = docMirror - .mustGetDocument(document) + .getDocument(document) .getTokenCursor(toplevelStartOffset + 1), - wordRange = document.getWordRangeAtPosition(position), - wordStartLocalOffset = + wordRange = document.getWordRangeAtPosition(position); + + util.assertIsDefined(wordRange, 'Expected a wordRange!'); + + const wordStartLocalOffset = document.offsetAt(wordRange.start) - toplevelStartOffset, wordEndLocalOffset = document.offsetAt(wordRange.end) - toplevelStartOffset, @@ -67,7 +76,7 @@ export async function provideCompletionItems( res = await client.complete( ns, text, - toplevelIsValidForm ? context : null + toplevelIsValidForm ? context : undefined ), results = res.completions || []; @@ -109,12 +118,17 @@ export default class CalvaCompletionItemProvider token: CancellationToken ) { if (util.getConnectedState()) { + const activeTextEditor = window.activeTextEditor; + util.assertIsDefined( + activeTextEditor, + 'Expected window to have activeTextEditor defined!' + ); const client = replSession.getSession( - util.getFileType(window.activeTextEditor.document) + util.getFileType(activeTextEditor.document) ); if (client) { await namespace.createNamespaceFromDocumentIfNotExists( - window.activeTextEditor.document + activeTextEditor.document ); const ns = namespace.getDocumentNamespace(); const result = await client.info( diff --git a/src/providers/hover.ts b/src/providers/hover.ts index 99e8059c3..7d3ad38f1 100644 --- a/src/providers/hover.ts +++ b/src/providers/hover.ts @@ -21,7 +21,7 @@ export async function provideHover( await namespace.createNamespaceFromDocumentIfNotExists(document); const res = await client.info(ns, text); const customREPLHoverSnippets = getConfig().customREPLHoverSnippets; - const hovers = []; + const hovers: vscode.MarkdownString[] = []; if ( !res.status.includes('error') && !res.status.includes('no-info') @@ -32,9 +32,13 @@ export async function provideHover( position ); - hovers.push(docsMd, clojureDocsMd); + hovers.push(docsMd); + + if (clojureDocsMd) { + hovers.push(clojureDocsMd); + } } - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const context = { ns, diff --git a/src/providers/infoparser.ts b/src/providers/infoparser.ts index cbe4d49d5..e81716288 100644 --- a/src/providers/infoparser.ts +++ b/src/providers/infoparser.ts @@ -6,14 +6,19 @@ import { import * as tokenCursor from '../cursor-doc/token-cursor'; import { getConfig } from '../config'; +export type Completion = + | [string, string] + | [MarkdownString, string | undefined] + | [undefined, undefined]; + export class REPLInfoParser { - private _name: string = undefined; + private _name: string | undefined = undefined; - private _arglist: string = undefined; + private _arglist: string | undefined = undefined; - private _formsString: string = undefined; + private _formsString: string | undefined = undefined; - private _docString: string = undefined; + private _docString: string | undefined = undefined; private _specialForm: boolean = false; @@ -55,7 +60,7 @@ export class REPLInfoParser { private getParameters( symbol: string, argList: string - ): ParameterInformation[] { + ): ParameterInformation[] | undefined { const offsets = this.getParameterOffsets(symbol, argList); if (offsets !== undefined) { return offsets.map((o) => { @@ -67,7 +72,7 @@ export class REPLInfoParser { private getParameterOffsets( symbol: string, argList: string - ): [number, number][] { + ): [number, number][] | undefined { const cursor: tokenCursor.LispTokenCursor = tokenCursor.createStringCursor(argList); if (cursor.downList()) { @@ -144,7 +149,7 @@ export class REPLInfoParser { return result; } - getCompletion(): [string | MarkdownString, string] { + getCompletion(): Completion { const name = new MarkdownString(this._docString); if (this._name !== '') { if (this._specialForm) { @@ -156,7 +161,7 @@ export class REPLInfoParser { return [undefined, undefined]; } - getSignatures(symbol: string): SignatureInformation[] { + getSignatures(symbol: string): SignatureInformation[] | undefined { if (this._name !== '') { const argLists = this._arglist ? this._arglist : this._formsString; if (argLists) { @@ -202,13 +207,13 @@ export function getHoverNotAvailable(text: string): string { return new REPLInfoParser({ name: text }).getHoverNotAvailable(); } -export function getCompletion(msg: any): [string | MarkdownString, string] { +export function getCompletion(msg: any): Completion { return new REPLInfoParser(msg).getCompletion(); } export function getSignatures( msg: any, symbol: string -): SignatureInformation[] { +): SignatureInformation[] | undefined { return new REPLInfoParser(msg).getSignatures(symbol); } diff --git a/src/providers/signature.ts b/src/providers/signature.ts index 22ee10ffc..c73f7a3a5 100644 --- a/src/providers/signature.ts +++ b/src/providers/signature.ts @@ -19,7 +19,7 @@ export class CalvaSignatureHelpProvider implements SignatureHelpProvider { document: TextDocument, position: Position, token: CancellationToken - ): Promise { + ): Promise { return provideSignatureHelp(document, position, token); } } @@ -28,7 +28,7 @@ export async function provideSignatureHelp( document: TextDocument, position: Position, _token: CancellationToken -): Promise { +): Promise { if (util.getConnectedState()) { const ns = namespace.getNamespace(document), idx = document.offsetAt(position), @@ -57,6 +57,10 @@ export async function provideSignatureHelp( (range) => range.contains(position) ), activeSignature = signatures[help.activeSignature]; + util.assertIsDefined( + activeSignature, + 'Expected activeSignature to be defined!' + ); help.activeParameter = activeSignature.label.match(/&/) !== null ? Math.min( @@ -70,12 +74,15 @@ export async function provideSignatureHelp( } } } - return null; + return undefined; } -function getCurrentArgsRanges(document: TextDocument, idx: number): Range[] { +function getCurrentArgsRanges( + document: TextDocument, + idx: number +): Range[] | undefined { const cursor: LispTokenCursor = docMirror - .mustGetDocument(document) + .getDocument(document) .getTokenCursor(idx), allRanges = cursor.rowColRangesForSexpsInList('('); @@ -86,7 +93,7 @@ function getCurrentArgsRanges(document: TextDocument, idx: number): Range[] { (previousRangeIndex > 1 && ['->', 'some->'].includes(previousFunction)) || (previousRangeIndex > 1 && - previousRangeIndex % 2 && + previousRangeIndex % 2 !== 0 && previousFunction === 'cond->'); if (allRanges !== undefined) { @@ -112,7 +119,7 @@ function getActiveSignatureIdx( function getSymbol(document: TextDocument, idx: number): string { const cursor: LispTokenCursor = docMirror - .mustGetDocument(document) + .getDocument(document) .getTokenCursor(idx); return cursor.getFunctionName(); } @@ -123,7 +130,7 @@ function coordsToRange(coords: [[number, number], [number, number]]): Range { function getPreviousRangeIndexAndFunction(document: TextDocument, idx: number) { const peekBehindCursor: LispTokenCursor = docMirror - .mustGetDocument(document) + .getDocument(document) .getTokenCursor(idx); peekBehindCursor.backwardFunction(1); const previousFunction = peekBehindCursor.getFunctionName(0), diff --git a/src/refresh.ts b/src/refresh.ts index 02677c210..dc2853828 100644 --- a/src/refresh.ts +++ b/src/refresh.ts @@ -21,7 +21,7 @@ function report(res, chan: vscode.OutputChannel) { } function refresh(document = {}) { - const doc = util.getDocument(document), + const doc = util.tryToGetDocument(document), client: NReplSession = replSession.getSession(util.getFileType(doc)), chan: vscode.OutputChannel = state.outputChannel(); @@ -36,7 +36,7 @@ function refresh(document = {}) { } function refreshAll(document = {}) { - const doc = util.getDocument(document), + const doc = util.tryToGetDocument(document), client: NReplSession = replSession.getSession(util.getFileType(doc)), chan: vscode.OutputChannel = state.outputChannel(); diff --git a/src/results-output/repl-history.ts b/src/results-output/repl-history.ts index c91962ca5..2d7a4db28 100644 --- a/src/results-output/repl-history.ts +++ b/src/results-output/repl-history.ts @@ -8,10 +8,11 @@ import { import type { ReplSessionType } from '../config'; import { isResultsDoc, getSessionType, getPrompt, append } from './results-doc'; import { addToHistory } from './util'; +import { isUndefined } from 'lodash'; const replHistoryCommandsActiveContext = 'calva:replHistoryCommandsActive'; -let historyIndex = null; -let lastTextAtPrompt = null; +let historyIndex: number | undefined = undefined; +let lastTextAtPrompt: string | undefined = undefined; function setReplHistoryCommandsActiveContext(editor: vscode.TextEditor): void { if (editor && util.getConnectedState() && isResultsDoc(editor.document)) { @@ -37,8 +38,8 @@ function setReplHistoryCommandsActiveContext(editor: vscode.TextEditor): void { } function resetState(): void { - historyIndex = null; - lastTextAtPrompt = null; + historyIndex = undefined; + lastTextAtPrompt = undefined; } function getHistoryKey(replSessionType: ReplSessionType): string { @@ -83,7 +84,7 @@ function clearHistory() { } function showReplHistoryEntry( - historyEntry: string, + historyEntry: string | undefined, resultsEditor: vscode.TextEditor ): void { const prompt = getPrompt(); @@ -117,7 +118,7 @@ function prependNewline(text: string) { } function showPreviousReplHistoryEntry(): void { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const doc = editor.document; const replSessionType = getSessionType(); const history = getHistory(replSessionType); @@ -128,10 +129,14 @@ function showPreviousReplHistoryEntry(): void { doc.getText(), getPrompt() ); - if (historyIndex === null) { + if (isUndefined(historyIndex)) { historyIndex = history.length; lastTextAtPrompt = textAtPrompt; } else { + util.assertIsDefined( + textAtPrompt, + 'Expected to find text at the prompt!' + ); updateReplHistory(replSessionType, history, textAtPrompt, historyIndex); } historyIndex--; @@ -139,7 +144,7 @@ function showPreviousReplHistoryEntry(): void { } function showNextReplHistoryEntry(): void { - const editor = util.mustGetActiveTextEditor(); + const editor = util.getActiveTextEditor(); const doc = editor.document; const replSessionType = getSessionType(); const history = getHistory(replSessionType); @@ -147,13 +152,21 @@ function showNextReplHistoryEntry(): void { return; } if (historyIndex === history.length - 1) { - historyIndex = null; + historyIndex = undefined; showReplHistoryEntry(lastTextAtPrompt, editor); } else { const textAtPrompt = getTextAfterLastOccurrenceOfSubstring( doc.getText(), getPrompt() ); + util.assertIsDefined( + textAtPrompt, + 'Expected to find text at the prompt!' + ); + util.assertIsDefined( + historyIndex, + 'Expected a value for historyIndex!' + ); updateReplHistory(replSessionType, history, textAtPrompt, historyIndex); historyIndex++; const nextHistoryEntry = history[historyIndex]; diff --git a/src/results-output/results-doc.ts b/src/results-output/results-doc.ts index 18e850b3c..77250b36d 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -42,6 +42,7 @@ export const CLJS_CONNECT_GREETINGS = function outputFileDir() { const projectRoot = state.getProjectRootUri(); + util.assertIsDefined(projectRoot, 'Expected there to be a project root!'); try { return vscode.Uri.joinPath(projectRoot, '.calva', 'output-window'); } catch { @@ -78,7 +79,7 @@ export function getPrompt(): string { return prompt; } -export function getNs(): string { +export function getNs(): string | undefined { return _sessionInfo[_sessionType].ns; } @@ -86,11 +87,11 @@ export function getSessionType(): ReplSessionType { return _sessionType; } -export function getSession(): NReplSession { +export function getSession(): NReplSession | undefined { return _sessionInfo[_sessionType].session; } -export function setSession(session: NReplSession, newNs: string): void { +export function setSession(session: NReplSession, newNs?: string): void { if (session) { if (session.replType) { _sessionType = session.replType; @@ -102,18 +103,17 @@ export function setSession(session: NReplSession, newNs: string): void { } } -export function isResultsDoc(doc: vscode.TextDocument): boolean { - return doc && path.basename(doc.fileName) === RESULTS_DOC_NAME; +export function isResultsDoc(doc?: vscode.TextDocument): boolean { + return !!doc && path.basename(doc.fileName) === RESULTS_DOC_NAME; } function getViewColumn(): vscode.ViewColumn { - const column: vscode.ViewColumn = state.extensionContext.workspaceState.get( - `outputWindowViewColumn` - ); + const column: vscode.ViewColumn | undefined = + state.extensionContext.workspaceState.get(`outputWindowViewColumn`); return column ? column : vscode.ViewColumn.Two; } -function setViewColumn(column: vscode.ViewColumn) { +function setViewColumn(column: vscode.ViewColumn | undefined) { return state.extensionContext.workspaceState.update( `outputWindowViewColumn`, column @@ -186,7 +186,7 @@ export async function initResultsDoc(): Promise { const document = event.textEditor.document; if (isResultsDoc(document)) { const idx = document.offsetAt(event.selections[0].active); - const mirrorDoc = docMirror.mustGetDocument(document); + const mirrorDoc = docMirror.getDocument(document); const selectionCursor = mirrorDoc.getTokenCursor(idx); selectionCursor.forwardWhitespace(); if (selectionCursor.atEnd()) { @@ -219,7 +219,7 @@ export async function initResultsDoc(): Promise { // If the output window is active when initResultsDoc is run, these contexts won't be set properly without the below // until the next time it's focused - const activeTextEditor = util.getActiveTextEditor(); + const activeTextEditor = util.tryToGetActiveTextEditor(); if (activeTextEditor && isResultsDoc(activeTextEditor.document)) { setContextForOutputWindowActive(true); replHistory.setReplHistoryCommandsActiveContext(activeTextEditor); @@ -255,8 +255,8 @@ export async function revealDocForCurrentNS(preserveFocus: boolean = true) { export async function setNamespaceFromCurrentFile() { const session = replSession.getSession(); - const ns = namespace.getNamespace(util.getDocument({})); - if (getNs() !== ns) { + const ns = namespace.getNamespace(util.tryToGetDocument({})); + if (getNs() !== ns && util.isDefined(ns)) { await session.eval("(in-ns '" + ns + ')', session.client.ns).value; } setSession(session, ns); @@ -265,8 +265,8 @@ export async function setNamespaceFromCurrentFile() { async function appendFormGrabbingSessionAndNS(topLevel: boolean) { const session = replSession.getSession(); - const ns = namespace.getNamespace(util.getDocument({})); - const editor = util.mustGetActiveTextEditor(); + const ns = namespace.getNamespace(util.tryToGetDocument({})); + const editor = util.getActiveTextEditor(); const doc = editor.document; const selection = editor.selection; let code = ''; @@ -301,7 +301,7 @@ let scrollToBottomSub: vscode.Disposable; export interface OnAppendedCallback { (insertLocation: vscode.Location, newPosition?: vscode.Location): any; } -let editQueue: [string, OnAppendedCallback][] = []; +let editQueue: [string, OnAppendedCallback | undefined][] = []; let applyingEdit = false; /* Because this function can be called several times asynchronously by the handling of incoming nrepl messages, we should never await it, because that await could possibly not return until way later, after edits that came in from elsewhere @@ -339,7 +339,7 @@ export function append(text: string, onAppended?: OnAppendedCallback): void { if (visibleResultsEditors.length == 0) { scrollToBottomSub = vscode.window.onDidChangeActiveTextEditor((editor) => { - if (isResultsDoc(editor.document)) { + if (editor && isResultsDoc(editor.document)) { util.scrollToBottom(editor); scrollToBottomSub.dispose(); } @@ -377,7 +377,10 @@ export function append(text: string, onAppended?: OnAppendedCallback): void { editQueue = remainingEditQueue; return append(textBatch.join('\n')); } else { - return append(...editQueue.shift()); + // we're sure there's a value in the queue at this point + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const [text, onAppended] = editQueue.shift()!; + return append(text, onAppended); } } }); @@ -394,7 +397,7 @@ export function discardPendingPrints(): void { export type OutputStacktraceEntry = { uri: vscode.Uri; line: number }; let _lastStacktrace: any[] = []; -let _lastStackTraceRange: vscode.Range; +let _lastStackTraceRange: vscode.Range | undefined; const _stacktraceEntries = {} as OutputStacktraceEntry; export function getStacktraceEntryForKey(key: string): OutputStacktraceEntry { @@ -435,7 +438,7 @@ export function markLastStacktraceRange(location: vscode.Location): void { _lastStackTraceRange = location.range; //new vscode.Range(newPosition, newPosition); } -export function getLastStackTraceRange(): vscode.Range { +export function getLastStackTraceRange(): vscode.Range | undefined { return _lastStackTraceRange; } diff --git a/src/results-output/util.ts b/src/results-output/util.ts index 69b6e63b5..8a0d965db 100644 --- a/src/results-output/util.ts +++ b/src/results-output/util.ts @@ -18,9 +18,9 @@ function formatAsLineComments(error: string): string { } function splitEditQueueForTextBatching( - editQueue: [string, OnAppendedCallback][], + editQueue: [string, OnAppendedCallback | undefined][], maxBatchSize: number = 1000 -): [string[], [string, OnAppendedCallback][]] { +): [string[], [string, OnAppendedCallback | undefined][]] { const textBatch = takeWhile(editQueue, (value, index) => { return index < maxBatchSize && !value[1]; }).map((value) => value[0]); diff --git a/src/select.ts b/src/select.ts index 82f456358..425039f22 100644 --- a/src/select.ts +++ b/src/select.ts @@ -16,9 +16,9 @@ function getFormSelection( doc: vscode.TextDocument, pos: vscode.Position, topLevel: boolean -): vscode.Selection { +): vscode.Selection | undefined { const idx = doc.offsetAt(pos); - const cursor = docMirror.mustGetDocument(doc).getTokenCursor(idx); + const cursor = docMirror.getDocument(doc).getTokenCursor(idx); const range = topLevel ? cursor.rangeForDefun(idx) : cursor.rangeForCurrentForm(idx); @@ -30,9 +30,9 @@ function getFormSelection( function getEnclosingFormSelection( doc: vscode.TextDocument, pos: vscode.Position -): vscode.Selection { +): vscode.Selection | undefined { const idx = doc.offsetAt(pos); - const cursor = docMirror.mustGetDocument(doc).getTokenCursor(idx); + const cursor = docMirror.getDocument(doc).getTokenCursor(idx); if (cursor.backwardList()) { cursor.backwardUpList(); const start = cursor.offsetStart; @@ -49,10 +49,10 @@ function selectForm( doc: vscode.TextDocument, pos: vscode.Position, topLevel: boolean - ) => vscode.Selection, + ) => vscode.Selection | undefined, toplevel: boolean ) { - const editor = util.mustGetActiveTextEditor(), + const editor = util.getActiveTextEditor(), doc = util.getDocument(document), selection = editor.selection; diff --git a/src/state.ts b/src/state.ts index 024541d6b..9d388b615 100644 --- a/src/state.ts +++ b/src/state.ts @@ -46,7 +46,7 @@ const PROJECT_DIR_KEY = 'connect.projectDir'; const PROJECT_DIR_URI_KEY = 'connect.projectDirNew'; const PROJECT_CONFIG_MAP = 'config'; -export function getProjectRootLocal(useCache = true): string { +export function getProjectRootLocal(useCache = true): string | undefined { if (useCache) { return getStateValue(PROJECT_DIR_KEY); } @@ -62,7 +62,7 @@ export function setProjectConfig(config) { return setStateValue(PROJECT_CONFIG_MAP, config); } -export function getProjectRootUri(useCache = true): vscode.Uri { +export function getProjectRootUri(useCache = true): vscode.Uri | undefined { if (useCache) { return getStateValue(PROJECT_DIR_URI_KEY); } @@ -72,8 +72,8 @@ const NON_PROJECT_DIR_KEY = 'calva.connect.nonProjectDir'; export async function getNonProjectRootDir( context: vscode.ExtensionContext -): Promise { - let root: vscode.Uri; +): Promise { + let root: vscode.Uri | undefined = undefined; if (!process.env['NEW_DRAMS']) { root = await context.globalState.get>( NON_PROJECT_DIR_KEY @@ -108,7 +108,7 @@ export async function getOrCreateNonProjectRoot( context: vscode.ExtensionContext, preferProjectDir = false ): Promise { - let root: vscode.Uri; + let root: vscode.Uri | undefined = undefined; if (preferProjectDir) { root = getProjectRootUri(); } @@ -130,8 +130,8 @@ export async function getOrCreateNonProjectRoot( return root; } -function getProjectWsFolder(): vscode.WorkspaceFolder { - const doc = util.getDocument({}); +function getProjectWsFolder(): vscode.WorkspaceFolder | undefined { + const doc = util.tryToGetDocument({}); if (doc) { const folder = vscode.workspace.getWorkspaceFolder(doc.uri); if (folder) { @@ -169,50 +169,51 @@ export async function initProjectDir(uri?: vscode.Uri): Promise { 'shadow-cljs.edn', 'deps.edn', ]; - const doc = util.getDocument({}); + const doc = util.tryToGetDocument({}); const workspaceFolder = getProjectWsFolder(); - await findLocalProjectRoot(projectFileNames, doc, workspaceFolder); + findLocalProjectRoot(projectFileNames, doc, workspaceFolder); await findProjectRootUri(projectFileNames, doc, workspaceFolder); } } function findLocalProjectRoot( - projectFileNames, - doc, - workspaceFolder -): Promise { + projectFileNames: string[], + doc: vscode.TextDocument | undefined, + workspaceFolder: vscode.WorkspaceFolder | undefined +): undefined { if (workspaceFolder) { let rootPath: string = path.resolve(workspaceFolder.uri.fsPath); setStateValue(PROJECT_DIR_KEY, rootPath); setStateValue(PROJECT_DIR_URI_KEY, workspaceFolder.uri); + const docPath = doc && path.dirname(doc.uri.fsPath); - let d = null; - let prev = null; - if (doc && path.dirname(doc.uri.fsPath) !== '.') { - d = path.dirname(doc.uri.fsPath); - } else { - d = workspaceFolder.uri.fsPath; - } - while (d !== prev) { - for (const projectFile in projectFileNames) { - const p = path.resolve(d, projectFileNames[projectFile]); - if (fs.existsSync(p)) { - rootPath = d; + let currentPath = + util.isDefined(docPath) && docPath !== '.' + ? docPath + : workspaceFolder.uri.fsPath; + + let previousPath = ''; + + do { + for (const projectFile of projectFileNames) { + const fullPath = path.resolve(currentPath, projectFile); + if (fs.existsSync(fullPath)) { + rootPath = currentPath; break; } } - if (d === rootPath) { + if (currentPath === rootPath) { break; } - prev = d; - d = path.resolve(d, '..'); - } + previousPath = currentPath; + currentPath = path.resolve(currentPath, '..'); + } while (currentPath !== previousPath); // at least be sure the the root folder contains a // supported project. - for (const projectFile in projectFileNames) { - const p = path.resolve(rootPath, projectFileNames[projectFile]); - if (fs.existsSync(p)) { + for (const projectFile of projectFileNames) { + const fullPath = path.resolve(rootPath, projectFile); + if (fs.existsSync(fullPath)) { setStateValue(PROJECT_DIR_KEY, rootPath); setStateValue(PROJECT_DIR_URI_KEY, vscode.Uri.file(rootPath)); return; diff --git a/src/statusbar.ts b/src/statusbar.ts index 36072f226..3c74359f8 100644 --- a/src/statusbar.ts +++ b/src/statusbar.ts @@ -37,12 +37,25 @@ function colorValue( section: string, currentConf: vscode.WorkspaceConfiguration ): string { + const configSection = currentConf.inspect(section); + + util.assertIsDefined( + configSection, + () => `Expected config section "${section}" to be defined!` + ); + const { defaultValue, globalValue, workspaceFolderValue, workspaceValue } = - currentConf.inspect(section); - return (workspaceFolderValue || - workspaceValue || - globalValue || - defaultValue) as string; + configSection; + + const value = + workspaceFolderValue || workspaceValue || globalValue || defaultValue; + + // Current behavior is to assert that this is a string even though it may + // not be. Maintaining current behavior for the moment but we should + // eventually do an assertion here or allow the function to return + // undefined. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion + return value!; } function update(context = state.extensionContext) { @@ -54,7 +67,7 @@ function update(context = state.extensionContext) { }` ); - const doc = util.getDocument({}), + const doc = util.tryToGetDocument({}), fileType = util.getFileType(doc), cljsBuild = getStateValue('cljsBuild'); @@ -65,23 +78,23 @@ function update(context = state.extensionContext) { //let disconnectedColor = "rgb(192,192,192)"; - const pprint = config.getConfig().prettyPrintingOptions.enabled; + const pprint = config.getConfig().prettyPrintingOptions?.enabled; prettyPrintToggle.text = 'pprint'; prettyPrintToggle.color = pprint ? undefined : color.inactive; prettyPrintToggle.tooltip = `Turn pretty printing ${pprint ? 'off' : 'on'}`; prettyPrintToggle.command = 'calva.togglePrettyPrint'; - typeStatus.command = null; + typeStatus.command = undefined; typeStatus.text = 'Disconnected'; typeStatus.tooltip = 'No active REPL session'; typeStatus.color = colorValue('disconnectedColor', currentConf); - connectionStatus.command = null; + connectionStatus.command = undefined; connectionStatus.tooltip = 'REPL connection status'; - cljsBuildStatus.text = null; + cljsBuildStatus.text = ''; cljsBuildStatus.command = 'calva.switchCljsBuild'; - cljsBuildStatus.tooltip = null; + cljsBuildStatus.tooltip = undefined; if (getStateValue('connected')) { connectionStatus.text = 'REPL $(zap)'; diff --git a/src/testRunner.ts b/src/testRunner.ts index 1d772bfc5..feba12d59 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -78,7 +78,7 @@ export function assertionName(result: cider.TestResult): string { function existingUriForNameSpace( controller: vscode.TestController, nsName: string -): vscode.Uri | null { +): vscode.Uri | undefined { return controller.items.get(nsName)?.uri; } @@ -114,6 +114,7 @@ async function onTestResult( .filter(cider.hasLineNumber) .map((a) => a.line) .sort(); + if (lines.length > 0) { test.range = new vscode.Range( lines[0] - 1, @@ -200,7 +201,7 @@ async function onTestResults( run.end(); } -function useTestExplorer(): boolean { +function useTestExplorer(): boolean | undefined { return vscode.workspace.getConfiguration('calva').get('useTestExplorer'); } @@ -215,7 +216,18 @@ function reportTests( diagnosticCollection.clear(); const recordDiagnostic = (result: cider.TestResult) => { + util.assertIsDefined( + result.line, + 'Expected cider test result to have a line!' + ); + + util.assertIsDefined( + result.file, + 'Expected cider test result to have a file!' + ); + const msg = cider.diagnosticMessage(result); + const err = new vscode.Diagnostic( new vscode.Range(result.line - 1, 0, result.line - 1, 1000), msg, @@ -348,7 +360,7 @@ async function runNamespaceTests( controller: vscode.TestController, document: vscode.TextDocument ) { - const doc = util.getDocument(document); + const doc = util.tryToGetDocument(document); if (outputWindow.isResultsDoc(doc)) { return; } @@ -359,7 +371,7 @@ async function runNamespaceTests( } function getTestUnderCursor() { - const editor = util.getActiveTextEditor(); + const editor = util.tryToGetActiveTextEditor(); if (editor) { return getText.currentTopLevelFunction( editor?.document, @@ -369,7 +381,7 @@ function getTestUnderCursor() { } async function runTestUnderCursor(controller: vscode.TestController) { - const doc = util.getDocument({}); + const doc = util.tryToGetDocument({}); const session = getSession(util.getFileType(doc)); const ns = namespace.getNamespace(doc); const test = getTestUnderCursor(); @@ -407,12 +419,11 @@ function runNamespaceTestsCommand(controller: vscode.TestController) { ); return; } - runNamespaceTests( - controller, - util.mustGetActiveTextEditor().document - ).catch((msg) => { - void vscode.window.showWarningMessage(msg); - }); + runNamespaceTests(controller, util.getActiveTextEditor().document).catch( + (msg) => { + void vscode.window.showWarningMessage(msg); + } + ); } async function rerunTests(controller: vscode.TestController, document = {}) { @@ -477,7 +488,7 @@ function initialize(controller: vscode.TestController): void { // The next steps here would be to turn request.exclude and requst.include // into a Cider var-query that can be passed to test-var query directly. const namespaces = util.distinct(runItems.map((ri) => ri[0])); - const doc = util.mustGetActiveTextEditor().document; + const doc = util.getActiveTextEditor().document; runNamespaceTestsImpl(controller, doc, namespaces).catch((msg) => { void vscode.window.showWarningMessage(msg); }); diff --git a/src/util/cursor-get-text.ts b/src/util/cursor-get-text.ts index fcb70f67b..ce46a7e69 100644 --- a/src/util/cursor-get-text.ts +++ b/src/util/cursor-get-text.ts @@ -4,7 +4,7 @@ import { EditableDocument } from '../cursor-doc/model'; -export type RangeAndText = [[number, number], string]; +export type RangeAndText = [[number, number], string] | [undefined, '']; export function currentTopLevelFunction( doc: EditableDocument, diff --git a/src/util/get-text.ts b/src/util/get-text.ts index 0f60520f2..9205aba49 100644 --- a/src/util/get-text.ts +++ b/src/util/get-text.ts @@ -5,7 +5,7 @@ import * as docMirror from '../doc-mirror/index'; import * as cursorTextGetter from './cursor-get-text'; import { EditableDocument } from '../cursor-doc/model'; -export type SelectionAndText = [vscode.Selection, string]; +export type SelectionAndText = [vscode.Selection | undefined, string]; function _currentFormText( doc: vscode.TextDocument, @@ -46,7 +46,7 @@ export function currentEnclosingFormText( export function currentFunction(doc: vscode.TextDocument): SelectionAndText { if (doc) { - const tokenCursor = docMirror.mustGetDocument(doc).getTokenCursor(); + const tokenCursor = docMirror.getDocument(doc).getTokenCursor(); const [start, end] = tokenCursor.getFunctionSexpRange(); if (start && end) { const startPos = doc.positionAt(start); @@ -67,7 +67,7 @@ function selectionAndText( pos: vscode.Position ): SelectionAndText { if (doc) { - const mirrorDoc = docMirror.mustGetDocument(doc); + const mirrorDoc = docMirror.getDocument(doc); const [range, text] = textGetter(mirrorDoc, doc.offsetAt(pos)); if (range) { return [select.selectionFromOffsetRange(doc, range), text]; @@ -117,7 +117,7 @@ function fromFn( cursorDocFn: (doc: EditableDocument, offset?: number) => [number, number] ): SelectionAndText { if (doc) { - const cursorDoc = docMirror.mustGetDocument(doc); + const cursorDoc = docMirror.getDocument(doc); const range = cursorDocFn(cursorDoc); const selection = select.selectionFromOffsetRange(doc, range); const text = doc.getText(selection); diff --git a/src/util/string.ts b/src/util/string.ts index 2a8ff9bea..c6fbcf2cb 100644 --- a/src/util/string.ts +++ b/src/util/string.ts @@ -27,10 +27,10 @@ function getIndexAfterLastNonWhitespace(text: string): number { function getTextAfterLastOccurrenceOfSubstring( text: string, substring: string -): string { +): string | undefined { const indexOfLastPrompt = text.lastIndexOf(substring); if (indexOfLastPrompt === -1) { - return null; + return undefined; } const indexOfEndOfPrompt = indexOfLastPrompt + substring.length; return text.substring(indexOfEndOfPrompt); diff --git a/src/utilities.ts b/src/utilities.ts index 8db1c3953..e6a2ec345 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -10,6 +10,7 @@ import * as outputWindow from './results-output/results-doc'; import * as cljsLib from '../out/cljs-lib/cljs-lib'; import * as url from 'url'; import { isUndefined } from 'lodash'; +import { isNullOrUndefined } from 'util'; const specialWords = ['-', '+', '/', '*']; //TODO: Add more here const syntaxQuoteSymbol = '`'; @@ -22,6 +23,21 @@ export function stripAnsi(str: string) { ); } +export const isDefined = (value: T | undefined | null): value is T => { + return !isNullOrUndefined(value); +}; + +// This needs to be a function and not an arrow function +// because assertion types are special. +export function assertIsDefined( + value: T | undefined | null, + message: string | (() => string) +): asserts value is T { + if (isNullOrUndefined(value)) { + throw new Error(typeof message === 'string' ? message : message()); + } +} + export function escapeStringRegexp(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -32,18 +48,15 @@ export function isNonEmptyString(value: any): boolean { async function quickPickSingle(opts: { values: string[]; - saveAs?: string; + saveAs: string; placeHolder: string; autoSelect?: boolean; }) { if (opts.values.length == 0) { return; } - let selected: string; - const saveAs: string = opts.saveAs ? `qps-${opts.saveAs}` : null; - if (saveAs) { - selected = state.extensionContext.workspaceState.get(saveAs); - } + const saveAs = `qps-${opts.saveAs}`; + const selected = state.extensionContext.workspaceState.get(saveAs); let result; if (opts.autoSelect && opts.values.length == 1) { @@ -60,14 +73,12 @@ async function quickPickSingle(opts: { async function quickPickMulti(opts: { values: string[]; - saveAs?: string; + saveAs: string; placeHolder: string; }) { - let selected: string[]; - const saveAs: string = opts.saveAs ? `qps-${opts.saveAs}` : null; - if (saveAs) { - selected = state.extensionContext.workspaceState.get(saveAs) || []; - } + const saveAs = `qps-${opts.saveAs}`; + const selected = + state.extensionContext.workspaceState.get(saveAs) || []; const result = await quickPick(opts.values, [], selected, { placeHolder: opts.placeHolder, canPickMany: true, @@ -95,19 +106,19 @@ async function quickPick( active: string[], selected: string[], options: vscode.QuickPickOptions -): Promise { +): Promise { const items = itemsToPick.map((x) => ({ label: x })); const qp = vscode.window.createQuickPick(); - qp.canSelectMany = options.canPickMany; + qp.canSelectMany = !!options.canPickMany; qp.placeholder = options.placeHolder; - qp.ignoreFocusOut = options.ignoreFocusOut; - qp.matchOnDescription = options.matchOnDescription; - qp.matchOnDetail = options.matchOnDetail; + qp.ignoreFocusOut = !!options.ignoreFocusOut; + qp.matchOnDescription = !!options.matchOnDescription; + qp.matchOnDetail = !!options.matchOnDetail; qp.items = items; qp.activeItems = items.filter((x) => active.indexOf(x.label) != -1); qp.selectedItems = items.filter((x) => selected.indexOf(x.label) != -1); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { qp.show(); qp.onDidAccept(() => { if (qp.canSelectMany) { @@ -172,10 +183,10 @@ function getWordAtPosition(document, position) { return text; } -function getDocument( - document: vscode.TextDocument | Record -): vscode.TextDocument { - const activeTextEditor = getActiveTextEditor(); +function tryToGetDocument( + document: vscode.TextDocument | Record | undefined +): vscode.TextDocument | undefined { + const activeTextEditor = tryToGetActiveTextEditor(); if ( document && Object.prototype.hasOwnProperty.call(document, 'fileName') @@ -190,14 +201,26 @@ function getDocument( const editor = vscode.window.visibleTextEditors.find( (editor) => editor.document && editor.document.languageId !== 'Log' ); - return editor ? editor.document : null; - } else { - return null; + return editor?.document; + } +} + +function getDocument( + document: vscode.TextDocument | Record +): vscode.TextDocument { + const doc = tryToGetDocument(document); + + if (isUndefined(doc)) { + throw new Error('Expected an activeTextEditor with a document!'); } + + return doc; } -function getFileType(document) { - const doc = getDocument(document); +function getFileType( + document: vscode.TextDocument | Record | undefined +) { + const doc = tryToGetDocument(document); if (doc) { return path.extname(doc.fileName).replace(/^\./, ''); @@ -298,7 +321,7 @@ function markError(error) { } const diagnostic = cljsLib.getStateValue('diagnosticCollection'), - editor = mustGetActiveTextEditor(); + editor = getActiveTextEditor(); //editor.selection = new vscode.Selection(position, position); const line = error.line - 1, @@ -347,7 +370,7 @@ function markWarning(warning) { } const diagnostic = cljsLib.getStateValue('diagnosticCollection'), - editor = mustGetActiveTextEditor(); + editor = getActiveTextEditor(); //editor.selection = new vscode.Selection(position, position); const line = Math.max(0, warning.line - 1), @@ -367,7 +390,9 @@ function markWarning(warning) { diagnostic.set(editor.document.uri, warnings); } -async function promptForUserInputString(prompt: string): Promise { +async function promptForUserInputString( + prompt: string +): Promise { return vscode.window.showInputBox({ prompt: prompt, ignoreFocusOut: true, @@ -388,8 +413,7 @@ function filterVisibleRanges( r.contains(visibleRange) ); }); - filtered = [].concat( - filtered, + filtered = filtered.concat( combine ? [ new vscode.Range( @@ -459,7 +483,7 @@ async function getJarContents(uri: vscode.Uri | string) { } function sortByPresetOrder(arr: any[], presetOrder: any[]) { - const result = []; + const result: any[] = []; presetOrder.forEach((preset) => { if (arr.indexOf(preset) != -1) { result.push(preset); @@ -543,12 +567,17 @@ const isWindows = process.platform === 'win32'; export async function isDocumentWritable( document: vscode.TextDocument -): Promise { +): Promise { if (!vscode.workspace.fs.isWritableFileSystem(document.uri.scheme)) { return false; } const fileStat = await vscode.workspace.fs.stat(document.uri); - return (fileStat.permissions & vscode.FilePermission.Readonly) !== 1; + + // I'm not sure in which cases fileStat permissions can be missing + // and so it's not clear what to do if it is. For the moment we can + // ignore this to maintain current behavior. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion + return (fileStat.permissions! & vscode.FilePermission.Readonly) !== 1; } // Returns the elements of coll with duplicates removed @@ -557,12 +586,12 @@ function distinct(coll: T[]): T[] { return [...new Set(coll)]; } -function getActiveTextEditor(): vscode.TextEditor | undefined { +function tryToGetActiveTextEditor(): vscode.TextEditor | undefined { return vscode.window.activeTextEditor; } -function mustGetActiveTextEditor(): vscode.TextEditor { - const editor = getActiveTextEditor(); +function getActiveTextEditor(): vscode.TextEditor { + const editor = tryToGetActiveTextEditor(); if (isUndefined(editor)) { throw new Error('Expected active text editor!'); @@ -578,6 +607,7 @@ function pathExists(path: string): boolean { export { distinct, getWordAtPosition, + tryToGetDocument, getDocument, getFileType, getLaunchingState, @@ -611,7 +641,7 @@ export { cljsLib, randomSlug, isWindows, + tryToGetActiveTextEditor, getActiveTextEditor, - mustGetActiveTextEditor, pathExists, }; diff --git a/src/when-contexts.ts b/src/when-contexts.ts index dc41941fc..9f1a6f484 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -11,7 +11,7 @@ export function setCursorContextIfChanged(editor: vscode.TextEditor) { !editor || !editor.document || editor.document.languageId !== 'clojure' || - editor !== util.getActiveTextEditor() + editor !== util.tryToGetActiveTextEditor() ) { return; } @@ -28,7 +28,7 @@ function determineCursorContexts( document: vscode.TextDocument, position: vscode.Position ): context.CursorContext[] { - const mirrorDoc = docMirror.mustGetDocument(document); + const mirrorDoc = docMirror.getDocument(document); return context.determineContexts(mirrorDoc, document.offsetAt(position)); }