diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a27c1afc..b75f269d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -216,7 +216,7 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "peacock.color": "#e48141", "calva.replConnectSequences": [ diff --git a/src/config.ts b/src/config.ts index 078675e3e..8d6e6eb30 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,7 @@ import { PrettyPrintingOptions } from './printer'; import { parseEdn } from '../out/cljs-lib/cljs-lib'; import * as state from './state'; import _ = require('lodash'); -import { isDefined } from './utilities'; +import { isDefined } from './type-checks'; const REPL_FILE_EXT = 'calva-repl'; const KEYBINDINGS_ENABLED_CONFIG_KEY = 'calva.keybindingsEnabled'; diff --git a/src/connector.ts b/src/connector.ts index 935c0711f..243622191 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -25,6 +25,7 @@ import * as clojureDocs from './clojuredocs'; import * as jszip from 'jszip'; import { addEdnConfig } from './config'; import { getJarContents } from './utilities'; +import { assertIsDefined, isNonEmptyString } from './type-checks'; async function readJarContent(uri: string) { try { @@ -39,6 +40,7 @@ async function readJarContent(uri: string) { } async function readRuntimeConfigs() { + assertIsDefined(nClient, 'Expected there to be an nREPL client!'); const classpath = await nClient.session.classpath().catch((e) => { console.error('readRuntimeConfigs:', e); }); @@ -55,7 +57,7 @@ async function readRuntimeConfigs() { // maybe we don't need to keep uri -> edn association, but it would make showing errors easier later return files - .filter(([_, config]) => util.isNonEmptyString(config)) + .filter(([_, config]) => isNonEmptyString(config)) .map(([_, config]) => addEdnConfig(config)); } } @@ -112,12 +114,9 @@ async function connectToHost(hostname: string, port: number, connectSequence: Re if (connectSequence.afterCLJReplJackInCode) { outputWindow.append(`\n; Evaluating 'afterCLJReplJackInCode'`); - await evaluateInOutputWindow( - connectSequence.afterCLJReplJackInCode, - 'clj', - outputWindow.getNs(), - {} - ); + const ns = outputWindow.getNs(); + assertIsDefined(ns, 'Expected outputWindow to have a namespace!'); + await evaluateInOutputWindow(connectSequence.afterCLJReplJackInCode, 'clj', ns, {}); } outputWindow.appendPrompt(); @@ -371,15 +370,23 @@ function createCLJSReplType( 'cljsReplTypeHasBuilds', cljsType.buildsRequired ); - let initCode = cljsType.connectCode, - build: string = null; + let initCode: typeof cljsType.connectCode | undefined = cljsType.connectCode, + build: string | null = null; if (menuSelections && menuSelections.cljsDefaultBuild && useDefaultBuild) { build = menuSelections.cljsDefaultBuild; useDefaultBuild = false; } else { if (typeof initCode === 'object' || initCode.includes('%BUILD%')) { + const buildsForSelection = startedBuilds + ? startedBuilds + : await figwheelOrShadowBuilds(cljsTypeName); + assertIsDefined( + buildsForSelection, + 'Expected there to be figwheel or shadowcljs builds!' + ); + build = await util.quickPickSingle({ - values: startedBuilds ? startedBuilds : await figwheelOrShadowBuilds(cljsTypeName), + values: buildsForSelection, placeHolder: 'Select which build to connect to', saveAs: `${state.getProjectRootUri().toString()}/${cljsTypeName.replace( ' ', @@ -415,12 +422,10 @@ function createCLJSReplType( ); }, connected: (result, out, err) => { - if (cljsType.isConnectedRegExp) { - return ( - [...out, result].find((x) => { - return x.search(cljsType.isConnectedRegExp) >= 0; - }) != undefined - ); + const { isConnectedRegExp } = cljsType; + + if (isConnectedRegExp) { + return [...out, result].find((x) => x.search(isConnectedRegExp) >= 0) !== undefined; } else { return true; } @@ -431,12 +436,15 @@ function createCLJSReplType( replType.start = async (session, name, checkFn) => { let startCode = cljsType.startCode; if (!hasStarted) { + assertIsDefined(startCode, 'Expected startCode to be defined!'); if (startCode.includes('%BUILDS')) { let builds: string[]; if (menuSelections && menuSelections.cljsLaunchBuilds) { builds = menuSelections.cljsLaunchBuilds; } else { const allBuilds = await figwheelOrShadowBuilds(cljsTypeName); + assertIsDefined(allBuilds, 'Expected there to be figwheel or shadowcljs builds!'); + builds = allBuilds.length <= 1 ? allBuilds @@ -494,11 +502,11 @@ function createCLJSReplType( } replType.started = (result, out, err) => { - if (cljsType.isReadyToStartRegExp && !hasStarted) { + const { isReadyToStartRegExp } = cljsType; + + if (isReadyToStartRegExp && !hasStarted) { const started = - [...out, ...err].find((x) => { - return x.search(cljsType.isReadyToStartRegExp) >= 0; - }) != undefined; + [...out, ...err].find((x) => x.search(isReadyToStartRegExp) >= 0) !== undefined; if (started) { hasStarted = true; } @@ -512,7 +520,7 @@ function createCLJSReplType( return replType; } -async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: string) { +async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: string | undefined) { outputWindow.append('; Creating cljs repl session...'); let newCljsSession = await session.clone(); newCljsSession.replType = 'cljs'; @@ -521,7 +529,8 @@ async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: st outputWindow.append( '; The Calva Connection Log might have more connection progress information.' ); - if (repl.start != undefined) { + if (repl.start !== undefined) { + assertIsDefined(repl.started, "Expected repl to have a 'started' check function!"); if (await repl.start(newCljsSession, repl.name, repl.started)) { state.analytics().logEvent('REPL', 'StartedCLJS', repl.name).send(); outputWindow.append('; Cljs builds started'); @@ -534,6 +543,9 @@ async function makeCljsSessionClone(session, repl: ReplType, projectTypeName: st return [null, null]; } } + + assertIsDefined(repl.connect, 'Expected repl to have a connect function!'); + if (await repl.connect(newCljsSession, repl.name, repl.connected)) { state.analytics().logEvent('REPL', 'ConnectedCLJS', repl.name).send(); setStateValue('cljs', (cljsSession = newCljsSession)); @@ -588,7 +600,7 @@ async function promptForNreplUrlAndConnect(port, connectSequence: ReplConnectSeq return true; } -export let nClient: NReplClient; +export let nClient: NReplClient | undefined; export let cljSession: NReplSession; export let cljsSession: NReplSession; @@ -637,7 +649,7 @@ export async function connect( return true; } -async function standaloneConnect(connectSequence: ReplConnectSequence) { +async function standaloneConnect(connectSequence: ReplConnectSequence | undefined) { await outputWindow.initResultsDoc(); await outputWindow.openResultsDoc(); @@ -701,6 +713,7 @@ export default { // the REPL client was connected. nClient.close(); } + liveShareSupport.didDisconnectRepl(); nClient = undefined; } @@ -708,13 +721,13 @@ export default { callback(); }, toggleCLJCSession: () => { - let newSession: NReplSession; + let newSession: NReplSession | undefined; if (getStateValue('connected')) { - if (replSession.getSession('cljc') == replSession.getSession('cljs')) { - newSession = replSession.getSession('clj'); - } else if (replSession.getSession('cljc') == replSession.getSession('clj')) { - newSession = replSession.getSession('cljs'); + if (replSession.tryToGetSession('cljc') == replSession.tryToGetSession('cljs')) { + newSession = replSession.tryToGetSession('clj'); + } else if (replSession.tryToGetSession('cljc') == replSession.tryToGetSession('clj')) { + newSession = replSession.tryToGetSession('cljs'); } setStateValue('cljc', newSession); if (outputWindow.isResultsDoc(util.getActiveTextEditor().document)) { @@ -726,9 +739,11 @@ export default { } }, switchCljsBuild: async () => { - const cljSession = replSession.getSession('clj'); - const cljsTypeName: string = state.extensionContext.workspaceState.get('selectedCljsTypeName'), - cljTypeName: string = state.extensionContext.workspaceState.get('selectedCljTypeName'); + const cljSession = replSession.tryToGetSession('clj'); + const cljsTypeName: string | undefined = + state.extensionContext.workspaceState.get('selectedCljsTypeName'), + cljTypeName: string | undefined = + state.extensionContext.workspaceState.get('selectedCljTypeName'); state.analytics().logEvent('REPL', 'switchCljsBuild', cljsTypeName).send(); const [session, build] = await makeCljsSessionClone( diff --git a/src/converters.ts b/src/converters.ts index 0ecdb2efe..d6667bd48 100644 --- a/src/converters.ts +++ b/src/converters.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import * as calvaLib from '../out/cljs-lib/cljs-lib'; +import { getActiveTextEditor } from './utilities'; type Js2CljsResult = { result: string; @@ -23,7 +24,7 @@ type Js2CljsInvalidResult = { const isJs2CljsResult = (input: any): input is Js2CljsResult => input.result !== undefined; export async function js2cljs() { - const editor = vscode.window.activeTextEditor; + const editor = getActiveTextEditor(); const selection = editor.selection; const doc = editor.document; const js = doc.getText( diff --git a/src/cursor-doc/clojure-lexer.ts b/src/cursor-doc/clojure-lexer.ts index 6ed535e4b..6a0285848 100644 --- a/src/cursor-doc/clojure-lexer.ts +++ b/src/cursor-doc/clojure-lexer.ts @@ -43,7 +43,7 @@ export function validPair(open: string, close: string): boolean { } export interface Token extends LexerToken { - state: ScannerState; + state: ScannerState | null; } // whitespace, excluding newlines @@ -174,7 +174,7 @@ export class Scanner { const tks: Token[] = []; this.state = state; let lex = (this.state.inString ? inString : toplevel).lex(line, this.maxLength); - let tk: LexerToken; + let tk: LexerToken | undefined; do { tk = lex.scan(); if (tk) { diff --git a/src/cursor-doc/indent.ts b/src/cursor-doc/indent.ts index 26b4d23a0..bad94c62e 100644 --- a/src/cursor-doc/indent.ts +++ b/src/cursor-doc/indent.ts @@ -18,7 +18,7 @@ const indentRules: IndentRules = { */ export interface IndentInformation { /** The first token in the expression (after the open paren/bracket etc.), as a raw string */ - first: string; + first: string | null; /** The indent immediately after the open paren/bracket etc */ startIndent: number; @@ -61,6 +61,17 @@ export function collectIndents( let lastIndent = 0; const indents: IndentInformation[] = []; const rules = config['cljfmt-options']['indents']; + const patterns = _.keys(rules); + const regexpPatterns = patterns.reduce((regexpMap, pattern) => { + const match = pattern.match(/^#"(.*)"$/); + + if (match) { + regexpMap[pattern] = RegExp(match[1]); + } + + return regexpMap; + }, {} as Record); + do { if (!cursor.backwardSexp()) { // this needs some work.. @@ -91,7 +102,10 @@ export function collectIndents( const pattern = isList && - _.find(_.keys(rules), (pattern) => pattern === token || testCljRe(pattern, token)); + patterns.find( + (pattern) => + pattern === token || (regexpPatterns[pattern] && regexpPatterns[pattern].test(token)) + ); const indentRule = pattern ? rules[pattern] : []; indents.unshift({ first: token, @@ -138,11 +152,6 @@ export function collectIndents( return indents; } -const testCljRe = (re, str) => { - const matches = re.match(/^#"(.*)"$/); - return matches && RegExp(matches[1]).test(str); -}; - /** Returns the expected newline indent for the given position, in characters. */ export function getIndent(document: EditableModel, offset: number, config?: any): number { if (!config) { diff --git a/src/cursor-doc/lexer.ts b/src/cursor-doc/lexer.ts index 4da61f106..edec1fb0e 100644 --- a/src/cursor-doc/lexer.ts +++ b/src/cursor-doc/lexer.ts @@ -3,6 +3,8 @@ * @module lexer */ +import { assertIsDefined } from '../type-checks'; + /** * The base Token class. Contains the token type, * the raw string of the token, and the offset into the input line. @@ -36,8 +38,8 @@ export class Lexer { constructor(public source: string, public rules: Rule[], private maxLength) {} /** Returns the next token in this lexer, or null if at the end. If the match fails, throws an Error. */ - scan(): Token { - let token = null, + scan(): Token | undefined { + let token: Token | undefined, length = 0; if (this.position < this.source.length) { if (this.source !== undefined && this.source.length < this.maxLength) { @@ -47,6 +49,7 @@ export class Lexer { const x = rule.r.exec(this.source); if (x && x[0].length > length && this.position + x[0].length == rule.r.lastIndex) { token = rule.fn(this, x); + assertIsDefined(token, 'Expected token!'); token.offset = this.position; token.raw = x[0]; length = x[0].length; @@ -62,9 +65,9 @@ export class Lexer { } } this.position += length; - if (token == null) { + if (token === undefined) { if (this.position == this.source.length) { - return null; + return undefined; } throw new Error( 'Unexpected character at ' + this.position + ': ' + JSON.stringify(this.source) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index fcca51ff1..71ec97206 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -155,12 +155,12 @@ export class LineInputModel implements EditableModel { } return x; }) - .filter((x) => x !== null) + .filter((x) => x !== null) as number[] ); this.insertedLines = new Set( Array.from(this.insertedLines) - .map((x): [number, number] => { + .map((x) => { const [a, b] = x; if (a > start && a < start + deleted) { return null; @@ -170,12 +170,12 @@ export class LineInputModel implements EditableModel { } return [a, b]; }) - .filter((x) => x !== null) + .filter((x) => x !== null) as [number, number][] ); this.deletedLines = new Set( Array.from(this.deletedLines) - .map((x): [number, number] => { + .map((x) => { const [a, b] = x; if (a > start && a < start + deleted) { return null; @@ -185,7 +185,7 @@ export class LineInputModel implements EditableModel { } return [a, b]; }) - .filter((x) => x !== null) + .filter((x) => x !== null) as [number, number][] ); } @@ -224,7 +224,7 @@ export class LineInputModel implements EditableModel { const seen = new Set(); this.dirtyLines.sort(); while (this.dirtyLines.length) { - let nextIdx = this.dirtyLines.shift(); + let nextIdx = this.dirtyLines.shift() as number; if (seen.has(nextIdx)) { continue; } // already processed. diff --git a/src/cursor-doc/paredit.ts b/src/cursor-doc/paredit.ts index cba3ac35e..bef27468b 100644 --- a/src/cursor-doc/paredit.ts +++ b/src/cursor-doc/paredit.ts @@ -1,3 +1,4 @@ +import { assertIsDefined } from '../type-checks'; import { validPair } from './clojure-lexer'; import { ModelEdit, EditableDocument, ModelEditSelection } from './model'; import { LispTokenCursor } from './token-cursor'; @@ -129,7 +130,7 @@ export function rangeForDefun( doc: EditableDocument, offset: number = doc.selection.active, commentCreatesTopLevel = true -): [number, number] { +): [number, number] | undefined { const cursor = doc.getTokenCursor(offset); return cursor.rangeForDefun(offset, commentCreatesTopLevel); } @@ -438,7 +439,7 @@ export function wrapSexpr( start: number = doc.selection.anchor, end: number = doc.selection.active, options = { skipFormat: false } -): Thenable { +): Thenable | undefined { const cursor = doc.getTokenCursor(end); if (cursor.withinString() && open == '"') { open = close = '\\"'; @@ -486,7 +487,7 @@ export function rewrapSexpr( close: string, start: number = doc.selection.anchor, end: number = doc.selection.active -): Thenable { +): Thenable | undefined { const cursor = doc.getTokenCursor(end); if (cursor.backwardList()) { const openStart = cursor.offsetStart - 1, @@ -530,7 +531,7 @@ export function splitSexp(doc: EditableDocument, start: number = doc.selection.a export function joinSexp( doc: EditableDocument, start: number = doc.selection.active -): Thenable { +): Thenable | undefined { const cursor = doc.getTokenCursor(start); cursor.backwardWhitespace(); const prevToken = cursor.getPrevToken(), @@ -560,7 +561,7 @@ export function spliceSexp( doc: EditableDocument, start: number = doc.selection.active, undoStopBefore = true -): Thenable { +): Thenable | undefined { const cursor = doc.getTokenCursor(start); // TODO: this should unwrap the string, not the enclosing list. @@ -671,7 +672,9 @@ export function backwardSlurpSexp( cursor.backwardList(); const tk = cursor.getPrevToken(); if (tk.type == 'open') { - const offset = cursor.clone().previous().offsetStart; + const previous = cursor.clone().previous(); + assertIsDefined(previous, 'Expected a token to be before the cursor!'); + const offset = previous.offsetStart; const open = cursor.getPrevToken().raw; cursor.previous(); cursor.backwardSexp(true, true); @@ -968,6 +971,7 @@ export function growSelectionStack(doc: EditableDocument, range: [number, number export function shrinkSelection(doc: EditableDocument) { if (doc.selectionStack.length) { const latest = doc.selectionStack.pop(); + assertIsDefined(latest, 'Expected a value in selectionStack!'); if ( doc.selectionStack.length && latest.anchor == doc.selection.anchor && @@ -988,7 +992,9 @@ export function raiseSexp( end = doc.selection.active ) { const cursor = doc.getTokenCursor(end); - const [formStart, formEnd] = cursor.rangeForCurrentForm(start); + const formRange = cursor.rangeForCurrentForm(start); + assertIsDefined(formRange, 'Expected to find a range for the current form!'); + const [formStart, formEnd] = formRange; const isCaretTrailing = formEnd - start < start - formStart; const startCursor = doc.getTokenCursor(formStart); const endCursor = startCursor.clone(); @@ -1142,6 +1148,7 @@ function currentSexpsRange( usePairs = false ): [number, number] { const currentSingleRange = cursor.rangeForCurrentForm(offset); + assertIsDefined(currentSingleRange, 'Expected to find a range for the current form!'); if (usePairs) { const ranges = cursor.rangesForSexpsInList(); if (ranges.length > 1) { @@ -1242,6 +1249,7 @@ export function collectWhitespaceInfo( ): WhitespaceInfo { const cursor = doc.getTokenCursor(p); const currentRange = cursor.rangeForCurrentForm(p); + assertIsDefined(currentRange, 'Expected to find a range for the current form!'); const leftWsRight = currentRange[0]; const leftWsCursor = doc.getTokenCursor(leftWsRight); const rightWsLeft = currentRange[1]; @@ -1271,6 +1279,7 @@ export function dragSexprBackwardUp(doc: EditableDocument, p = doc.selection.act const cursor = doc.getTokenCursor(p); const currentRange = cursor.rangeForCurrentForm(p); if (cursor.backwardList() && cursor.backwardUpList()) { + assertIsDefined(currentRange, 'Expected to find a range for the current form!'); const listStart = cursor.offsetStart; const newPosOffset = p - currentRange[0]; const newCursorPos = listStart + newPosOffset; @@ -1310,6 +1319,7 @@ export function dragSexprBackwardUp(doc: EditableDocument, p = doc.selection.act export function dragSexprForwardDown(doc: EditableDocument, p = doc.selection.active) { const wsInfo = collectWhitespaceInfo(doc, p); const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); + assertIsDefined(currentRange, 'Expected to find a range for the current form!'); const newPosOffset = p - currentRange[0]; const cursor = doc.getTokenCursor(currentRange[0]); while (cursor.forwardSexp()) { @@ -1348,6 +1358,7 @@ export function dragSexprForwardUp(doc: EditableDocument, p = doc.selection.acti const cursor = doc.getTokenCursor(p); const currentRange = cursor.rangeForCurrentForm(p); if (cursor.forwardList() && cursor.upList()) { + assertIsDefined(currentRange, 'Expected to find a range for the current form!'); const listEnd = cursor.offsetStart; const newPosOffset = p - currentRange[0]; const listWsInfo = collectWhitespaceInfo(doc, listEnd); @@ -1377,6 +1388,7 @@ export function dragSexprForwardUp(doc: EditableDocument, p = doc.selection.acti export function dragSexprBackwardDown(doc: EditableDocument, p = doc.selection.active) { const wsInfo = collectWhitespaceInfo(doc, p); const currentRange = doc.getTokenCursor(p).rangeForCurrentForm(p); + assertIsDefined(currentRange, 'Expected to find a range for the current form!'); const newPosOffset = p - currentRange[0]; const cursor = doc.getTokenCursor(currentRange[1]); while (cursor.backwardSexp()) { @@ -1425,6 +1437,7 @@ export function addRichComment(doc: EditableDocument, p = doc.selection.active, const richComment = `(comment\n ${contents ? adaptContentsToRichComment(contents) : ''}\n )`; let cursor = doc.getTokenCursor(p); const topLevelRange = rangeForDefun(doc, p, false); + assertIsDefined(topLevelRange, 'Expected to find a range for the current defun!'); const isInsideForm = !(p <= topLevelRange[0] || p >= topLevelRange[1]); const checkIfAtStartCursor = doc.getTokenCursor(p); checkIfAtStartCursor.backwardWhitespace(true); diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index 97cfab567..6043074d6 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -27,7 +27,7 @@ export class TokenCursor { } /** Return the position */ - get rowCol() { + get rowCol(): [number, number] { return [this.line, this.getToken().offset]; } @@ -117,26 +117,21 @@ export class TokenCursor { * If you are particular about which list type that should be considered, supply an `openingBracket`. */ -function _rangesForSexpsInList( +const collectRanges = < + StartFieldKey extends keyof LispTokenCursor, + EndFieldKey extends keyof LispTokenCursor +>( cursor: LispTokenCursor, - useRowCol = false, - openingBracket?: string -): [number, number][] | [[number, number], [number, number]][] { - if (openingBracket !== undefined) { - if (!cursor.backwardListOfType(openingBracket)) { - return undefined; - } - } else { - if (!cursor.backwardList()) { - return undefined; - } - } - const ranges = []; + cursorStartField: StartFieldKey, + cursorEndField: EndFieldKey +): [LispTokenCursor[StartFieldKey], LispTokenCursor[EndFieldKey]][] => { + const ranges: [LispTokenCursor[StartFieldKey], LispTokenCursor[EndFieldKey]][] = []; // TODO: Figure out how to do this ignore skipping more generally in forward/backward this or that. let ignoreCounter = 0; + while (true) { cursor.forwardWhitespace(); - const start = useRowCol ? cursor.rowCol : cursor.offsetStart; + const start = cursor[cursorStartField]; if (cursor.getToken().type === 'ignore') { ignoreCounter++; cursor.forwardSexp(); @@ -144,7 +139,7 @@ function _rangesForSexpsInList( } if (cursor.forwardSexp()) { if (ignoreCounter === 0) { - const end = useRowCol ? cursor.rowCol : cursor.offsetStart; + const end = cursor[cursorEndField]; ranges.push([start, end]); } else { ignoreCounter--; @@ -154,6 +149,28 @@ function _rangesForSexpsInList( } } return ranges; +}; + +function _rangesForSexpsInList( + cursor: LispTokenCursor, + useRowCol = false, + openingBracket?: string +): [number, number][] | [[number, number], [number, number]][] | undefined { + if (openingBracket !== undefined) { + if (!cursor.backwardListOfType(openingBracket)) { + return undefined; + } + } else { + if (!cursor.backwardList()) { + return undefined; + } + } + + if (useRowCol) { + return collectRanges(cursor, 'rowCol', 'rowCol'); + } else { + return collectRanges(cursor, 'offsetStart', 'offsetStart'); + } } export class LispTokenCursor extends TokenCursor { @@ -238,7 +255,7 @@ export class LispTokenCursor extends TokenCursor { */ forwardSexp(skipComments = true, skipMetadata = false, skipIgnoredForms = false): boolean { // TODO: Consider using a proper bracket stack - const stack = []; + const stack: string[] = []; let isMetadata = false; this.forwardWhitespace(skipComments); if (this.getToken().type === 'close') { @@ -280,7 +297,7 @@ export class LispTokenCursor extends TokenCursor { break; case 'close': { const close = token.raw; - let open: string; + let open: string | undefined; while ((open = stack.pop())) { if (validPair(open, close)) { this.next(); @@ -305,6 +322,8 @@ export class LispTokenCursor extends TokenCursor { break; } } + + return false; } /** @@ -322,7 +341,7 @@ export class LispTokenCursor extends TokenCursor { skipIgnoredForms = false, skipReaders = true ) { - const stack = []; + const stack: string[] = []; this.backwardWhitespace(skipComments); if (this.getPrevToken().type === 'open') { return false; @@ -364,7 +383,7 @@ export class LispTokenCursor extends TokenCursor { break; case 'open': { const open = tk.raw; - let close: string; + let close: string | undefined; while ((close = stack.pop())) { if (validPair(open, close)) { break; @@ -511,9 +530,9 @@ export class LispTokenCursor extends TokenCursor { * If you are particular about which type of list, supply the `openingBracket`. * @param openingBracket */ - rangeForList(depth: number, openingBracket?: string): [number, number] { + rangeForList(depth: number, openingBracket?: string): [number, number] | undefined { const cursor = this.clone(); - let range: [number, number] = undefined; + let range: [number, number] | undefined = undefined; for (let i = 0; i < depth; i++) { if (openingBracket === undefined) { if (!(cursor.backwardList() && cursor.backwardUpList())) { @@ -631,8 +650,8 @@ export class LispTokenCursor extends TokenCursor { * 8. Else, return `undefined`. * @param offset the current cursor (caret) offset in the document */ - rangeForCurrentForm(offset: number): [number, number] { - let afterCurrentFormOffset: number; + rangeForCurrentForm(offset: number): [number, number] | undefined { + let afterCurrentFormOffset: number | undefined; // console.log(-1, offset); // 0. If `offset` is within or before, a symbol, literal or keyword @@ -765,9 +784,9 @@ export class LispTokenCursor extends TokenCursor { return [currentFormCursor.offsetStart, afterCurrentFormOffset]; } - rangeForDefun(offset: number, commentCreatesTopLevel = true): [number, number] { + rangeForDefun(offset: number, commentCreatesTopLevel = true): [number, number] | undefined { const cursor = this.doc.getTokenCursor(offset); - let lastCandidateRange: [number, number] = cursor.rangeForCurrentForm(offset); + let lastCandidateRange: [number, number] | undefined = cursor.rangeForCurrentForm(offset); while (cursor.forwardList() && cursor.upList()) { const commentCursor = cursor.clone(); commentCursor.backwardDownList(); @@ -898,7 +917,7 @@ export class LispTokenCursor extends TokenCursor { * @param levels how many levels of functions to dig up. * @returns the function name, or undefined if there is no function there. */ - getFunctionName(levels: number = 0): string { + getFunctionName(levels: number = 0): string | undefined { const cursor = this.clone(); if (cursor.backwardFunction(levels)) { cursor.forwardWhitespace(); @@ -914,7 +933,7 @@ export class LispTokenCursor extends TokenCursor { * @param levels how many levels of functions to dig up. * @returns the range of the function sexp/form, or undefined if there is no function there. */ - getFunctionSexpRange(levels: number = 0): [number, number] { + getFunctionSexpRange(levels: number = 0): [number, number] | [undefined, undefined] { const cursor = this.clone(); if (cursor.backwardFunction(levels)) { cursor.forwardWhitespace(); diff --git a/src/cursor-doc/undo.ts b/src/cursor-doc/undo.ts index cf5f124e1..4a2eb763c 100644 --- a/src/cursor-doc/undo.ts +++ b/src/cursor-doc/undo.ts @@ -114,8 +114,10 @@ export class UndoManager { undo(c: T) { if (this.undos.length) { const step = this.undos.pop(); - step.undo(c); - this.redos.push(step); + if (step) { + step.undo(c); + this.redos.push(step); + } } } @@ -123,8 +125,10 @@ export class UndoManager { redo(c: T) { if (this.redos.length) { const step = this.redos.pop(); - step.redo(c); - this.undos.push(step); + if (step) { + step.redo(c); + this.undos.push(step); + } } } } diff --git a/src/custom-snippets.ts b/src/custom-snippets.ts index 7e47b132c..e6b25dd9c 100644 --- a/src/custom-snippets.ts +++ b/src/custom-snippets.ts @@ -31,8 +31,8 @@ async function evaluateCodeOrKey(codeOrKey?: string) { if (snippets.length < 1) { snippets = getConfig().customREPLCommandSnippets; } - const snippetsDict = {}; - const snippetsKeyDict = {}; + const snippetsMap = new Map(); + const snippetsKeyMap = new Map(); const snippetsMenuItems: string[] = []; const editorNS = editor && editor.document && editor.document.languageId === 'clojure' @@ -49,14 +49,14 @@ async function evaluateCodeOrKey(codeOrKey?: string) { if (undefs.length > 0) { configErrors.push({ name: c.name, keys: undefs }); } - const entry = { ...c }; + const entry: customREPLCommandSnippet = { ...c }; entry.ns = entry.ns ? entry.ns : editorNS; entry.repl = entry.repl ? entry.repl : editorRepl; const prefix = entry.key !== undefined ? `${entry.key}: ` : ''; const item = `${prefix}${entry.name} (${entry.repl})`; snippetsMenuItems.push(item); - snippetsDict[item] = entry; - snippetsKeyDict[entry.key] = item; + snippetsMap.set(item, entry); + snippetsKeyMap.set(entry.key, item); }); if (configErrors.length > 0) { @@ -68,7 +68,7 @@ async function evaluateCodeOrKey(codeOrKey?: string) { return; } - let pick: string; + let pick: string | undefined; if (codeOrKey === undefined) { // Without codeOrKey always show snippets menu if (snippetsMenuItems.length > 0) { @@ -94,21 +94,23 @@ async function evaluateCodeOrKey(codeOrKey?: string) { } if (pick === undefined) { // still no pick, but codeOrKey might be one - pick = snippetsKeyDict[codeOrKey]; + pick = snippetsKeyMap.get(codeOrKey); } - const code = pick !== undefined ? snippetsDict[pick].snippet : codeOrKey; - const ns = pick !== undefined ? snippetsDict[pick].ns : editorNS; - const repl = pick !== undefined ? snippetsDict[pick].repl : editorRepl; + + const replSnippet = snippetsMap.get(pick); + + const code = pick !== undefined ? replSnippet?.snippet : codeOrKey; + const ns = pick !== undefined ? replSnippet?.ns : editorNS; + const repl = pick !== undefined ? replSnippet?.repl : editorRepl; const options = {}; if (pick !== undefined) { - options['evaluationSendCodeToOutputWindow'] = - snippetsDict[pick].evaluationSendCodeToOutputWindow; + options['evaluationSendCodeToOutputWindow'] = replSnippet?.evaluationSendCodeToOutputWindow; // don't allow addToHistory if we don't show the code but are inside the repl options['addToHistory'] = state.extensionContext.workspaceState.get('outputWindowActive') && - !snippetsDict[pick].evaluationSendCodeToOutputWindow + !replSnippet?.evaluationSendCodeToOutputWindow ? false : undefined; } diff --git a/src/debugger/calva-debug.ts b/src/debugger/calva-debug.ts index d0bc22765..0786c4271 100644 --- a/src/debugger/calva-debug.ts +++ b/src/debugger/calva-debug.ts @@ -35,8 +35,8 @@ import annotations from '../providers/annotations'; import { NReplSession } from '../nrepl'; import debugDecorations from './decorations'; import { setStateValue, getStateValue } from '../../out/cljs-lib/cljs-lib'; -import * as util from '../utilities'; import * as replSession from '../nrepl/repl-session'; +import { assertIsDefined } from '../type-checks'; const CALVA_DEBUG_CONFIGURATION: DebugConfiguration = { type: 'clojure', @@ -84,8 +84,8 @@ class CalvaDebugSession extends LoggingDebugSession { response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments ): void { - this.setDebuggerLinesStartAt1(args.linesStartAt1); - this.setDebuggerColumnsStartAt1(args.columnsStartAt1); + this.setDebuggerLinesStartAt1(!!args.linesStartAt1); + this.setDebuggerColumnsStartAt1(!!args.columnsStartAt1); // Build and return the capabilities of this debug adapter response.body = { @@ -100,7 +100,7 @@ class CalvaDebugSession extends LoggingDebugSession { response: DebugProtocol.AttachResponse, args: DebugProtocol.AttachRequestArguments ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); this.sendResponse(response); state @@ -114,7 +114,7 @@ class CalvaDebugSession extends LoggingDebugSession { args: DebugProtocol.ContinueArguments, request?: DebugProtocol.Request ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); if (cljSession) { const { id, key } = getStateValue(DEBUG_RESPONSE_KEY); @@ -146,7 +146,7 @@ class CalvaDebugSession extends LoggingDebugSession { args: DebugProtocol.NextArguments, request?: DebugProtocol.Request ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); if (cljSession) { const { id, key } = getStateValue(DEBUG_RESPONSE_KEY); @@ -169,7 +169,7 @@ class CalvaDebugSession extends LoggingDebugSession { args: DebugProtocol.StepInArguments, request?: DebugProtocol.Request ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); if (cljSession) { const { id, key } = getStateValue(DEBUG_RESPONSE_KEY); @@ -192,7 +192,7 @@ class CalvaDebugSession extends LoggingDebugSession { args: DebugProtocol.StepOutArguments, request?: DebugProtocol.Request ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); if (cljSession) { const { id, key } = getStateValue(DEBUG_RESPONSE_KEY); @@ -271,6 +271,7 @@ class CalvaDebugSession extends LoggingDebugSession { // Pass scheme in path argument to Source contructor so that if it's a jar file it's handled correctly const source = new Source(basename(debugResponse.file), debugResponse.file); const name = tokenCursor.getFunctionName(); + assertIsDefined(name, 'Expected to find a function name!'); const stackFrames = [new StackFrame(0, name, source, line + 1, column + 1)]; response.body = { @@ -324,7 +325,7 @@ class CalvaDebugSession extends LoggingDebugSession { args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request ): void { - const cljSession = replSession.getSession(CLOJURE_SESSION_NAME); + const cljSession = replSession.tryToGetSession(CLOJURE_SESSION_NAME); if (cljSession) { const { id, key } = getStateValue(DEBUG_RESPONSE_KEY); diff --git a/src/debugger/decorations.ts b/src/debugger/decorations.ts index e2b40716c..6bac0d72a 100644 --- a/src/debugger/decorations.ts +++ b/src/debugger/decorations.ts @@ -7,6 +7,7 @@ import * as util from '../utilities'; import lsp from '../lsp/main'; import { getStateValue } from '../../out/cljs-lib/cljs-lib'; import * as replSession from '../nrepl/repl-session'; +import { assertIsDefined } from '../type-checks'; let enabled = false; @@ -33,7 +34,7 @@ const instrumentedSymbolDecorationType = vscode.window.createTextEditorDecoratio async function update( editor: vscode.TextEditor, - cljSession: NReplSession, + cljSession: NReplSession | undefined, lspClient: LanguageClient ): Promise { if (/(\.clj)$/.test(editor.document.fileName)) { @@ -49,6 +50,7 @@ async function update( const docUri = vscode.Uri.parse(namespacePath, true); const decodedDocUri = decodeURIComponent(docUri.toString()); const docSymbols = (await lsp.getDocumentSymbols(lspClient, decodedDocUri))[0].children; + assertIsDefined(docSymbols, 'Expected to get document symbols from the LSP server!'); const instrumentedDocSymbols = docSymbols.filter((s) => instrumentedDefs.includes(s.name) ); @@ -118,7 +120,7 @@ function triggerUpdateAndRenderDecorations() { const editor = util.tryToGetActiveTextEditor(); if (editor) { timeout = setTimeout(() => { - const cljSession = replSession.getSession('clj'); + const cljSession = replSession.tryToGetSession('clj'); const lspClient = getStateValue(lsp.LSP_CLIENT_KEY); void update(editor, cljSession, lspClient).then(renderInAllVisibleEditors); }, 50); diff --git a/src/debugger/util.ts b/src/debugger/util.ts index 80e4aa177..bf531af15 100644 --- a/src/debugger/util.ts +++ b/src/debugger/util.ts @@ -1,7 +1,10 @@ import { LispTokenCursor } from '../cursor-doc/token-cursor'; +import { assertIsDefined } from '../type-checks'; function moveCursorPastStringInList(tokenCursor: LispTokenCursor, s: string): void { - const [listOffsetStart, listOffsetEnd] = tokenCursor.rangeForList(1); + const range = tokenCursor.rangeForList(1); + assertIsDefined(range, 'Expected range to be found!'); + const [listOffsetStart, listOffsetEnd] = range; const text = tokenCursor.doc.getText(listOffsetStart, listOffsetEnd - 1); const stringIndexInList = text.indexOf(s); @@ -22,7 +25,9 @@ function moveTokenCursorToBreakpoint( debugResponse: any ): LispTokenCursor { const errorMessage = 'Error finding position of breakpoint'; - const [_, defunEnd] = tokenCursor.rangeForDefun(tokenCursor.offsetStart); + const range = tokenCursor.rangeForDefun(tokenCursor.offsetStart); + assertIsDefined(range, 'Expected range to be found!'); + const [_, defunEnd] = range; let inSyntaxQuote = false; const coor = [...debugResponse.coor]; // Copy the array so we do not modify the one stored in state diff --git a/src/doc-mirror/index.ts b/src/doc-mirror/index.ts index 4d6ec962f..7c1376321 100644 --- a/src/doc-mirror/index.ts +++ b/src/doc-mirror/index.ts @@ -12,6 +12,7 @@ import { ModelEditSelection, } from '../cursor-doc/model'; import { isUndefined } from 'lodash'; +import { assertIsDefined } from '../type-checks'; const documents = new Map(); @@ -169,18 +170,21 @@ export class MirroredDocument implements EditableDocument { } public delete(): Thenable { - return vscode.commands.executeCommand('deleteRight'); + return vscode.commands.executeCommand('deleteRight').then((v) => !!v); } public backspace(): Thenable { - return vscode.commands.executeCommand('deleteLeft'); + return vscode.commands.executeCommand('deleteLeft').then((v) => !!v); } } let registered = false; function processChanges(event: vscode.TextDocumentChangeEvent) { - const model = documents.get(event.document).model; + const mirrorDoc = documents.get(event.document); + assertIsDefined(mirrorDoc, 'Expected to find a mirror document!'); + const model = mirrorDoc.model; + for (const change of event.contentChanges) { // vscode may have a \r\n marker, so it's line offsets are all wrong. const myStartOffset = diff --git a/src/evaluate.ts b/src/evaluate.ts index 8a8c2386d..899fdf5b7 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -15,6 +15,7 @@ import { getStateValue } from '../out/cljs-lib/cljs-lib'; import { getConfig } from './config'; import * as replSession from './nrepl/repl-session'; import * as getText from './util/get-text'; +import { assertIsDefined } from './type-checks'; function interruptAllEvaluations() { if (util.getConnectedState()) { @@ -86,6 +87,7 @@ async function evaluateCode( if (code.length > 0) { if (addToHistory) { + assertIsDefined(session.replType, 'Expected session to have a repl type!'); replHistory.addToReplHistory(session.replType, code); replHistory.resetState(); } @@ -153,6 +155,7 @@ async function evaluateCode( if (context.stacktrace) { outputWindow.saveStacktrace(context.stacktrace); outputWindow.append(errMsg, (_, afterResultLocation) => { + assertIsDefined(afterResultLocation, 'Expected there to be a location!'); outputWindow.markLastStacktraceRange(afterResultLocation); }); } else { @@ -193,6 +196,7 @@ async function evaluateCode( } } if (context.stacktrace && context.stacktrace.stacktrace) { + assertIsDefined(afterResultLocation, 'Expected there to be a location!'); outputWindow.markLastStacktraceRange(afterResultLocation); } }); @@ -225,7 +229,7 @@ async function evaluateSelection(document = {}, options) { const line = codeSelection.start.line; const column = codeSelection.start.character; const filePath = doc.fileName; - const session = replSession.getSession(util.getFileType(doc)); + const session = replSession.tryToGetSession(util.getFileType(doc)); if (code.length > 0) { if (options.debug) { @@ -405,10 +409,12 @@ async function loadFile( const doc = util.tryToGetDocument(document); const fileType = util.getFileType(doc); const ns = namespace.getNamespace(doc); - const session = replSession.getSession(util.getFileType(doc)); + const session = replSession.tryToGetSession(util.getFileType(doc)); if (doc && doc.languageId == 'clojure' && fileType != 'edn' && getStateValue('connected')) { state.analytics().logEvent('Evaluation', 'LoadFile').send(); + assertIsDefined(session, 'Expected there to be a repl session!'); + assertIsDefined(ns, 'Expected there to be a namespace!'); const docUri = outputWindow.isResultsDoc(doc) ? await namespace.getUriForNamespace(session, ns) : doc.uri; @@ -447,7 +453,7 @@ async function loadFile( async function evaluateUser(code: string) { const fileType = util.getFileType(util.tryToGetDocument({})), - session = replSession.getSession(fileType); + session = replSession.tryToGetSession(fileType); if (session) { try { await session.eval(code, session.client.ns).value; @@ -469,11 +475,12 @@ async function requireREPLUtilitiesCommand() { sessionType = replSession.getReplSessionTypeFromState(), form = sessionType == 'cljs' ? CLJS_FORM : CLJ_FORM, fileType = util.getFileType(util.tryToGetDocument({})), - session = replSession.getSession(fileType); + session = replSession.tryToGetSession(fileType); if (session) { try { await namespace.createNamespaceFromDocumentIfNotExists(util.tryToGetDocument({})); + assertIsDefined(ns, 'Expected there to be a namespace!'); await session.switchNS(ns); await session.eval(form, ns).value; chan.appendLine(`REPL utilities are now available in namespace ${ns}.`); @@ -503,6 +510,7 @@ async function togglePrettyPrint() { const config = vscode.workspace.getConfiguration('calva'), pprintConfigKey = 'prettyPrintingOptions', pprintOptions = config.get(pprintConfigKey); + assertIsDefined(pprintOptions, 'Expected there to be pprint options!'); pprintOptions.enabled = !pprintOptions.enabled; if (pprintOptions.enabled && !(pprintOptions.printEngine || pprintOptions.printFn)) { pprintOptions.printEngine = 'pprint'; @@ -545,9 +553,10 @@ export async function evaluateInOutputWindow( const outputDocument = await outputWindow.openResultsDoc(); const evalPos = outputDocument.positionAt(outputDocument.getText().length); try { - const session = replSession.getSession(sessionType); + const session = replSession.tryToGetSession(sessionType); replSession.updateReplSessionType(); if (outputWindow.getNs() !== ns) { + assertIsDefined(session, 'Expected there to be a repl session!'); await session.switchNS(ns); outputWindow.setSession(session, ns); if (options.evaluationSendCodeToOutputWindow !== false) { @@ -574,6 +583,7 @@ export type customREPLCommandSnippet = { snippet: string; repl?: string; ns?: string; + evaluationSendCodeToOutputWindow?: boolean; }; export default { diff --git a/src/extension-test/unit/common/text-notation.ts b/src/extension-test/unit/common/text-notation.ts index ed674cf4f..3f7419dac 100644 --- a/src/extension-test/unit/common/text-notation.ts +++ b/src/extension-test/unit/common/text-notation.ts @@ -12,7 +12,9 @@ import * as model from '../../../cursor-doc/model'; * * Selections with direction left->right are denoted with `|<|` at the range boundaries */ -function textNotationToTextAndSelection(s: string): [string, { anchor: number; active: number }] { +function textNotationToTextAndSelection( + s: string +): [string, { anchor: number; active: number | undefined }] { const text = s.replace(/•/g, '\n').replace(/\|?[<>]?\|/g, ''); let anchor: undefined | number = undefined; let active: undefined | number = undefined; diff --git a/src/extension-test/unit/cursor-doc/clojure-lexer-test.ts b/src/extension-test/unit/cursor-doc/clojure-lexer-test.ts index fe2d46db6..e09307b0d 100644 --- a/src/extension-test/unit/cursor-doc/clojure-lexer-test.ts +++ b/src/extension-test/unit/cursor-doc/clojure-lexer-test.ts @@ -677,7 +677,7 @@ describe('Scanner', () => { if (!['reader', 'junk'].includes(rule.name)) { expect(x).toBeNull(); } else { - expect(x.length).toBe(1); + expect(x?.length).toBe(1); } }); }); diff --git a/src/extension-test/unit/results-output/util-test.ts b/src/extension-test/unit/results-output/util-test.ts index e2b445c71..274e76d15 100644 --- a/src/extension-test/unit/results-output/util-test.ts +++ b/src/extension-test/unit/results-output/util-test.ts @@ -25,7 +25,7 @@ describe('addToHistory', () => { }); it('should not push null to history array', () => { const history = []; - const newHistory = util.addToHistory(history, null); + const newHistory = util.addToHistory(history, undefined); expect(newHistory.length).toBe(history.length); }); }); diff --git a/src/extension.ts b/src/extension.ts index b4f49d3e1..1dc7c1eee 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -37,6 +37,7 @@ import * as nreplLogging from './nrepl/logging'; import * as converters from './converters'; import * as clojureDocs from './clojuredocs'; +import { assertIsDefined } from './type-checks'; async function onDidSave(testController: vscode.TestController, document: vscode.TextDocument) { const { evaluate, test } = config.getConfig(); @@ -49,7 +50,9 @@ async function onDidSave(testController: vscode.TestController, document: vscode state.analytics().logEvent('Calva', 'OnSaveTest').send(); } else if (evaluate) { if (!outputWindow.isResultsDoc(document)) { - await eval.loadFile(document, config.getConfig().prettyPrintingOptions); + const pprintOptions = config.getConfig().prettyPrintingOptions; + assertIsDefined(pprintOptions, 'Expected there to be pprint options!'); + await eval.loadFile(document, pprintOptions); outputWindow.appendPrompt(); state.analytics().logEvent('Calva', 'OnSaveLoad').send(); } @@ -62,7 +65,7 @@ function onDidOpen(document) { } } -function onDidChangeEditorOrSelection(editor: vscode.TextEditor) { +function onDidChangeEditorOrSelection(editor: vscode.TextEditor | undefined) { replHistory.setReplHistoryCommandsActiveContext(editor); whenContexts.setCursorContextIfChanged(editor); } @@ -106,7 +109,14 @@ async function activate(context: vscode.ExtensionContext) { setStateValue('analytics', new Analytics(context)); state.analytics().logPath('/start').logEvent('LifeCycle', 'Started').send(); - model.initScanner(vscode.workspace.getConfiguration('editor').get('maxTokenizationLineLength')); + const maxTokenizationLineLength = vscode.workspace + .getConfiguration('editor') + .get('maxTokenizationLineLength'); + assertIsDefined( + maxTokenizationLineLength, + 'Expected there to be a maxTokenizationLineLength set in the editor config' + ); + model.initScanner(maxTokenizationLineLength); const chan = state.outputChannel(); @@ -118,6 +128,10 @@ async function activate(context: vscode.ExtensionContext) { const clojureExtension = vscode.extensions.getExtension('avli.clojure'); const customCljsRepl = config.getConfig().customCljsRepl; const replConnectSequences = config.getConfig().replConnectSequences; + assertIsDefined( + replConnectSequences, + 'Expected there to be a repl connect sequence in the config!' + ); const BUTTON_GOTO_DOC = 'Open the docs'; const BUTTON_OK = 'Got it'; const VIM_DOC_URL = 'https://calva.io/vim/'; @@ -231,7 +245,9 @@ async function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push( vscode.commands.registerCommand('calva.loadFile', async () => { - await eval.loadFile({}, config.getConfig().prettyPrintingOptions); + const pprintOptions = config.getConfig().prettyPrintingOptions; + assertIsDefined(pprintOptions, 'Expected pprint options in the config!'); + await eval.loadFile({}, pprintOptions); return new Promise((resolve) => { outputWindow.appendPrompt(resolve); }); diff --git a/src/highlight/src/extension.ts b/src/highlight/src/extension.ts index 77ed1d0b1..e9b59ed0a 100755 --- a/src/highlight/src/extension.ts +++ b/src/highlight/src/extension.ts @@ -1,17 +1,17 @@ import * as vscode from 'vscode'; import { Position, Range } from 'vscode'; import * as isEqual from 'lodash.isequal'; -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 { tryToGetActiveTextEditor, getActiveTextEditor } from '../../utilities'; +import { assertIsDefined } from '../../type-checks'; type StackItem = { char: string; start: Position; end: Position; - pair_idx: number; + pair_idx: number | undefined; opens_comment_form?: boolean; }; @@ -23,7 +23,12 @@ function is_clojure(editor) { } // Exported for integration testing purposes -export let activeEditor: vscode.TextEditor; +export let activeEditor: vscode.TextEditor | undefined; + +const definedActiveEditor = () => { + assertIsDefined(activeEditor, 'Expected there to be an active text editor set!'); + return activeEditor; +}; let lastHighlightedEditor, rainbowColors, @@ -45,8 +50,8 @@ let lastHighlightedEditor, pairsBack: Map = new Map(), pairsForward: Map = new Map(), placedGuidesColor: Map = new Map(), - rainbowTimer = undefined, - matchTimer = undefined, + rainbowTimer: NodeJS.Timeout | undefined = undefined, + matchTimer: NodeJS.Timeout | undefined = undefined, dirty = false; reloadConfig(); @@ -57,7 +62,7 @@ function decorationType(opts) { } function colorDecorationType(color) { - if (isArray(color)) { + if (Array.isArray(color)) { return decorationType({ light: { color: color[0] }, dark: { color: color[1] }, @@ -68,7 +73,7 @@ function colorDecorationType(color) { } function guidesDecorationType_(color, isActive: boolean): vscode.TextEditorDecorationType { - if (isArray(color)) { + if (Array.isArray(color)) { return decorationType({ light: { borderWidth: `0; border-right-width: ${ @@ -105,23 +110,26 @@ function activeGuidesDecorationType(color): vscode.TextEditorDecorationType { } function reset_styles() { + const editor = activeEditor; + assertIsDefined(editor, 'Expected there to be an active text editor!'); + if (rainbowTypes) { - rainbowTypes.forEach((type) => activeEditor.setDecorations(type, [])); + rainbowTypes.forEach((type) => editor.setDecorations(type, [])); } rainbowTypes = rainbowColors.map(colorDecorationType); if (rainbowGuidesTypes) { - rainbowGuidesTypes.forEach((type) => activeEditor.setDecorations(type, [])); + rainbowGuidesTypes.forEach((type) => editor.setDecorations(type, [])); } rainbowGuidesTypes = rainbowColors.map(guidesDecorationType); if (activeGuidesTypes) { - activeGuidesTypes.forEach((type) => activeEditor.setDecorations(type, [])); + activeGuidesTypes.forEach((type) => editor.setDecorations(type, [])); } activeGuidesTypes = rainbowColors.map(activeGuidesDecorationType); if (misplacedType) { - activeEditor.setDecorations(misplacedType, []); + editor.setDecorations(misplacedType, []); } misplacedType = decorationType( misplacedBracketStyle || { @@ -133,7 +141,7 @@ function reset_styles() { ); if (matchedType) { - activeEditor.setDecorations(matchedType, []); + editor.setDecorations(matchedType, []); } matchedType = decorationType( matchedBracketStyle || { @@ -143,12 +151,12 @@ function reset_styles() { ); if (commentFormType) { - activeEditor.setDecorations(commentFormType, []); + editor.setDecorations(commentFormType, []); } commentFormType = decorationType(commentFormStyle || { fontStyle: 'italic' }); if (ignoredFormType) { - activeEditor.setDecorations(ignoredFormType, []); + editor.setDecorations(ignoredFormType, []); } ignoredFormType = decorationType(ignoredFormStyle || { textDecoration: 'none; opacity: 0.5' }); @@ -231,13 +239,16 @@ function updateRainbowBrackets() { reset_styles(); } - const doc = activeEditor.document, + const editor = activeEditor; + assertIsDefined(editor, 'Expected there to be an active text editor!'); + + const doc = editor.document, mirrorDoc = docMirror.getDocument(doc), - rainbow = rainbowTypes.map(() => []), + rainbow = rainbowTypes.map(() => [] as { range: Range }[]), rainbowGuides = rainbowTypes.map(() => []), - misplaced = [], - comment_forms = [], - ignores = [], + misplaced: { range: Range }[] = [], + comment_forms: Range[] = [], + ignores: Range[] = [], len = rainbowTypes.length, colorsEnabled = enableBracketColors && len > 0, guideColorsEnabled = useRainbowIndentGuides && len > 0, @@ -250,7 +261,7 @@ function updateRainbowBrackets() { pairsBack = new Map(); pairsForward = new Map(); placedGuidesColor = new Map(); - activeEditor.visibleRanges.forEach((range) => { + editor.visibleRanges.forEach((range) => { // Find the visible forms const startOffset = doc.offsetAt(range.start), endOffset = doc.offsetAt(range.end), @@ -289,7 +300,7 @@ function updateRainbowBrackets() { } else if (token.type === 'ignore') { const ignoreCursor = cursor.clone(); let ignore_counter = 0; - const ignore_start = activeEditor.document.positionAt(ignoreCursor.offsetStart); + const ignore_start = editor.document.positionAt(ignoreCursor.offsetStart); while (ignoreCursor.getToken().type === 'ignore') { ignore_counter++; ignoreCursor.next(); @@ -298,7 +309,7 @@ function updateRainbowBrackets() { for (let i = 0; i < ignore_counter; i++) { ignoreCursor.forwardSexp(true, true, true); } - const ignore_end = activeEditor.document.positionAt(ignoreCursor.offsetStart); + const ignore_end = editor.document.positionAt(ignoreCursor.offsetStart); ignores.push(new Range(ignore_start, ignore_end)); } } @@ -318,10 +329,11 @@ function updateRainbowBrackets() { if (token.type === 'open') { const readerCursor = cursor.clone(); readerCursor.backwardThroughAnyReader(); - const start = activeEditor.document.positionAt(readerCursor.offsetStart), - end = activeEditor.document.positionAt(cursor.offsetEnd), + + const start = editor.document.positionAt(readerCursor.offsetStart), + end = editor.document.positionAt(cursor.offsetEnd), openRange = new Range(start, end), - openString = activeEditor.document.getText(openRange); + openString = editor.document.getText(openRange); if (colorsEnabled) { const decoration = { range: openRange }; rainbow[colorIndex(stack_depth)].push(decoration); @@ -336,11 +348,12 @@ function updateRainbowBrackets() { }); continue; } else if (token.type === 'close') { - const pos = activeEditor.document.positionAt(cursor.offsetStart), + const pos = editor.document.positionAt(cursor.offsetStart), decoration = { range: new Range(pos, pos.translate(0, 1)) }; let pair_idx = stack.length - 1; - while (pair_idx >= 0 && stack[pair_idx].pair_idx !== undefined) { - pair_idx = stack[pair_idx].pair_idx - 1; + const stackPairIdx = stack[pair_idx]; + while (pair_idx >= 0 && stackPairIdx.pair_idx !== undefined) { + pair_idx = stackPairIdx.pair_idx - 1; } if (pair_idx === undefined || pair_idx < 0 || !validPair(stack[pair_idx].char, char)) { misplaced.push(decoration); @@ -359,9 +372,9 @@ function updateRainbowBrackets() { pair_idx: pair_idx, }); pairsBack.set(position_str(pos), [opening, closing]); - const startOffset = activeEditor.document.offsetAt(pair.start); + const startOffset = editor.document.offsetAt(pair.start); for (let i = 0; i < pair.char.length; ++i) { - pairsForward.set(position_str(activeEditor.document.positionAt(startOffset + i)), [ + pairsForward.set(position_str(editor.document.positionAt(startOffset + i)), [ opening, closing, ]); @@ -373,6 +386,7 @@ function updateRainbowBrackets() { if (guideColorsEnabled || activeGuideEnabled) { const matchPos = pos.translate(0, 1); const openSelection = matchBefore(new vscode.Selection(matchPos, matchPos)); + assertIsDefined(openSelection, 'Expected openSelection to be defined!'); const openSelectionPos = openSelection[0].start; const guideLength = decorateGuide( doc, @@ -391,14 +405,14 @@ function updateRainbowBrackets() { }); for (let i = 0; i < rainbowTypes.length; ++i) { - activeEditor.setDecorations(rainbowTypes[i], rainbow[i]); + editor.setDecorations(rainbowTypes[i], rainbow[i]); if (guideColorsEnabled) { - activeEditor.setDecorations(rainbowGuidesTypes[i], rainbowGuides[i]); + editor.setDecorations(rainbowGuidesTypes[i], rainbowGuides[i]); } } - activeEditor.setDecorations(misplacedType, misplaced); - activeEditor.setDecorations(commentFormType, comment_forms); - activeEditor.setDecorations(ignoredFormType, ignores); + editor.setDecorations(misplacedType, misplaced); + editor.setDecorations(commentFormType, comment_forms); + editor.setDecorations(ignoredFormType, ignores); matchPairs(); if (activeGuideEnabled) { decorateActiveGuides(); @@ -428,8 +442,11 @@ function matchPairs() { return; } + const editor = activeEditor; + assertIsDefined(editor, 'Expected there to be an active text editor!'); + const matches: { range: vscode.Range }[] = []; - activeEditor.selections.forEach((selection) => { + editor.selections.forEach((selection) => { const match_before = matchBefore(selection), match_after = matchAfter(selection); if (match_before) { @@ -441,7 +458,7 @@ function matchPairs() { matches.push({ range: match_after[1] }); } }); - activeEditor.setDecorations(matchedType, matches); + editor.setDecorations(matchedType, matches); } function decorateGuide( @@ -453,7 +470,8 @@ function decorateGuide( let guideLength = 0; for (let lineDelta = 1; lineDelta <= endPos.line - startPos.line; lineDelta++) { const guidePos = startPos.translate(lineDelta, 0); - if (doc.lineAt(guidePos).text.match(/^ */)[0].length >= startPos.character) { + const leadingSpacesMatch = doc.lineAt(guidePos).text.match(/^ */); + if (leadingSpacesMatch && leadingSpacesMatch[0].length >= startPos.character) { const guidesDecoration = { range: new Range(guidePos, guidePos) }; guides.push(guidesDecoration); guideLength++; @@ -464,12 +482,13 @@ function decorateGuide( function decorateActiveGuides() { const activeGuides = []; - activeEditor = getActiveTextEditor(); + const editor = getActiveTextEditor(); + activeEditor = editor; if (activeGuidesTypes) { - activeGuidesTypes.forEach((type) => activeEditor.setDecorations(type, [])); + activeGuidesTypes.forEach((type) => editor.setDecorations(type, [])); } - activeEditor.selections.forEach((selection) => { - const doc = activeEditor.document; + editor.selections.forEach((selection) => { + const doc = editor.document; const mirrorDoc = docMirror.getDocument(doc); const cursor = mirrorDoc.getTokenCursor(doc.offsetAt(selection.start)); const visitedEndPositions = [selection.start]; @@ -490,7 +509,7 @@ function decorateActiveGuides() { if (colorIndex !== undefined) { if (guideRange.contains(selection)) { decorateGuide(doc, startPos, endPos, activeGuides); - activeEditor.setDecorations(activeGuidesTypes[colorIndex], activeGuides); + editor.setDecorations(activeGuidesTypes[colorIndex], activeGuides); } break; } @@ -539,7 +558,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidChangeTextDocument( (event) => { - if (is_clojure(activeEditor) && event.document === activeEditor.document) { + if (is_clojure(activeEditor) && event.document === activeEditor?.document) { scheduleRainbowBrackets(); } }, diff --git a/src/live-share.ts b/src/live-share.ts index 1f989d1e6..abf36bb92 100644 --- a/src/live-share.ts +++ b/src/live-share.ts @@ -3,12 +3,12 @@ import * as vsls from 'vsls'; import * as config from './config'; // Keeps hold of the LiveShare API instance, so that it is requested only once. -let liveShare: vsls.LiveShare = null; +let liveShare: vsls.LiveShare | null = null; // Keeps hold of the LiveShare listener, to prevent it from being disposed immediately. -let liveShareListener: Disposable = null; +let liveShareListener: Disposable | null = null; -let connectedPort: number = null; +let connectedPort: number | null = null; let jackedIn = false; const sharedPorts: Map = new Map(); diff --git a/src/lsp/download.ts b/src/lsp/download.ts index 845740bf0..71bae4892 100644 --- a/src/lsp/download.ts +++ b/src/lsp/download.ts @@ -18,12 +18,25 @@ async function getLatestVersion(): Promise { } } +const zipFileNames = { + darwin: 'clojure-lsp-native-macos-amd64.zip', + linux: 'clojure-lsp-native-static-linux-amd64.zip', + win32: 'clojure-lsp-native-windows-amd64.zip', +}; + +const validPlatforms = Object.keys(zipFileNames); +type ValidPlatform = keyof typeof zipFileNames; + +function assertValidPlatform(platform: string): asserts platform is ValidPlatform { + if (!validPlatforms.includes(platform)) { + throw new Error(`Expected a valid clojure-lsp platform but got ${platform}`); + } +} + function getZipFileName(platform: string): string { - return { - darwin: 'clojure-lsp-native-macos-amd64.zip', - linux: 'clojure-lsp-native-static-linux-amd64.zip', - win32: 'clojure-lsp-native-windows-amd64.zip', - }[platform]; + assertValidPlatform(platform); + + return zipFileNames[platform]; } function getZipFilePath(extensionPath: string, platform: string): string { @@ -88,6 +101,7 @@ async function unzipFile(zipFilePath: string, extensionPath: string): Promise { + console.log({ extensionPath, version, platform: process.platform }); const zipFileName = getZipFileName(process.platform); const urlPath = `/clojure-lsp/clojure-lsp/releases/download/${version}/${zipFileName}`; const zipFilePath = getZipFilePath(extensionPath, process.platform); diff --git a/src/lsp/main.ts b/src/lsp/main.ts index 40950c597..f73a73e58 100644 --- a/src/lsp/main.ts +++ b/src/lsp/main.ts @@ -23,6 +23,7 @@ import { provideHover } from '../providers/hover'; import { provideSignatureHelp } from '../providers/signature'; import { isResultsDoc } from '../results-output/results-doc'; import { MessageItem } from 'vscode'; +import { assertIsDefined } from '../type-checks'; const LSP_CLIENT_KEY = 'lspClient'; const LSP_CLIENT_KEY_ERROR = 'lspClientError'; @@ -32,7 +33,7 @@ const SERVER_NOT_RUNNING_OR_INITIALIZED_MESSAGE = const lspStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); let serverVersion: string; let extensionContext: vscode.ExtensionContext; -let clojureLspPath: string; +let clojureLspPath: string | undefined; let testTreeHandler: TestTreeHandler; let lspCommandsRegistered = false; @@ -124,7 +125,11 @@ function createClient(clojureLspPath: string, fallbackFolder: FallbackFolder): L provideCodeActions(document, range, context, token, next) { return next(document, range, context, token); }, - provideCodeLenses: async (document, token, next): Promise => { + provideCodeLenses: async ( + document, + token, + next + ): Promise => { if (config.getConfig().referencesCodeLensEnabled) { return await next(document, token); } @@ -137,7 +142,7 @@ function createClient(clojureLspPath: string, fallbackFolder: FallbackFolder): L return null; }, async provideHover(document, position, token, next) { - let hover: vscode.Hover; + let hover: vscode.Hover | null | undefined; try { hover = await provideHover(document, position); } catch { @@ -266,7 +271,7 @@ function registerLspCommand(command: ClojureLspCommand): vscode.Disposable { const docUri = `${document.uri.scheme}://${document.uri.path}`; const params = [docUri, line, column]; const extraParam = command.extraParamFn ? await command.extraParamFn() : undefined; - if (!command.extraParamFn || (command.extraParamFn && extraParam)) { + if (!command.extraParamFn || extraParam) { sendCommandRequest(command.command, extraParam ? [...params, extraParam] : params); } } @@ -360,10 +365,12 @@ enum FolderType { FROM_NO_CLOJURE_FILE = 3, } -type FallbackFolder = { - uri: vscode.Uri; - type: FolderType; -}; +type FallbackFolder = + | { uri: undefined; type: FolderType.VSCODE } + | { + uri: vscode.Uri; + type: FolderType; + }; /** * Figures out a ”best fit” rootUri for use when starting the clojure-lsp @@ -382,8 +389,8 @@ async function getFallbackFolder(): Promise { } const activeEditor = vscode.window.activeTextEditor; - let clojureFilePath: string; - let folderType: FolderType; + let clojureFilePath: string | undefined; + let folderType: FolderType | undefined; if (activeEditor && activeEditor.document?.languageId === 'clojure') { folderType = activeEditor.document.isUntitled ? FolderType.FROM_UNTITLED_FILE @@ -394,8 +401,10 @@ async function getFallbackFolder(): Promise { } else { for (const document of vscode.workspace.textDocuments) { if (document.languageId === 'clojure') { - folderType = document.isUntitled ? FolderType.FROM_UNTITLED_FILE : FolderType.FROM_FS_FILE; - if (!document.isUntitled) { + if (document.isUntitled) { + folderType = FolderType.FROM_UNTITLED_FILE; + } else { + folderType = FolderType.FROM_FS_FILE; clojureFilePath = document.uri.fsPath; } } @@ -422,6 +431,8 @@ async function getFallbackFolder(): Promise { } } + assertIsDefined(folderType, 'Expected there to be a folderType at this point!'); + return { uri: fallbackFolder, type: folderType, @@ -477,6 +488,7 @@ async function startClient(fallbackFolder: FallbackFolder): Promise { }); } setStateValue(LSP_CLIENT_KEY, undefined); + assertIsDefined(clojureLspPath, 'Expected there to be a clojure LSP path!'); const client = createClient(clojureLspPath, fallbackFolder); console.log('Starting clojure-lsp at', clojureLspPath); @@ -515,11 +527,14 @@ async function startClient(fallbackFolder: FallbackFolder): Promise { // A quickPick that expects the same input as showInformationMessage does // TODO: How do we make it satisfy the messageFunc interface above? -function quickPick(message: string, actions: { title: string }[]): Promise<{ title: string }> { +function quickPick( + message: string, + actions: { title: string }[] +): Promise<{ title: string } | undefined> { const qp = vscode.window.createQuickPick(); qp.items = actions.map((item) => ({ label: item.title })); qp.title = message; - return new Promise<{ title: string }>((resolve, _reject) => { + return new Promise<{ title: string } | undefined>((resolve, _reject) => { qp.show(); qp.onDidAccept(() => { if (qp.selectedItems.length > 0) { @@ -649,7 +664,7 @@ async function activate(context: vscode.ExtensionContext, handler: TestTreeHandl } } -async function maybeDownloadLspServer(forceDownLoad = false): Promise { +async function maybeDownloadLspServer(forceDownLoad = false): Promise { const userConfiguredClojureLspPath = config.getConfig().clojureLspPath; if (userConfiguredClojureLspPath !== '') { clojureLspPath = userConfiguredClojureLspPath; @@ -672,11 +687,12 @@ async function downloadLSPServerCommand() { async function ensureServerDownloaded(forceDownLoad = false): Promise { const currentVersion = readVersionFile(extensionContext.extensionPath); - const configuredVersion: string = config.getConfig().clojureLspVersion; + const configuredVersion: string | undefined = config.getConfig().clojureLspVersion; clojureLspPath = getClojureLspPath(extensionContext.extensionPath, util.isWindows); - const downloadVersion = ['', 'latest'].includes(configuredVersion) - ? await getLatestVersion() - : configuredVersion; + const downloadVersion = + configuredVersion === undefined || ['', 'latest'].includes(configuredVersion) + ? await getLatestVersion() + : configuredVersion; if ( (currentVersion !== downloadVersion && downloadVersion !== '') || forceDownLoad || @@ -787,7 +803,7 @@ export async function getCljFmtConfig(): Promise { function showMenu(items: vscode.QuickPickItem[], commands: Record) { void vscode.window.showQuickPick(items, { title: 'clojure-lsp' }).then((v) => { - if (commands[v.label]) { + if (v && commands[v.label]) { void vscode.commands.executeCommand(commands[v.label]); } }); diff --git a/src/namespace.ts b/src/namespace.ts index 477b45ac5..d8e1c5515 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -7,11 +7,12 @@ import * as outputWindow from './results-output/results-doc'; import * as utilities from './utilities'; import * as replSession from './nrepl/repl-session'; import { NReplSession } from './nrepl'; +import { assertIsDefined } from './type-checks'; export function getNamespace(doc?: vscode.TextDocument) { if (outputWindow.isResultsDoc(doc)) { const outputWindowNs = outputWindow.getNs(); - utilities.assertIsDefined(outputWindowNs, 'Expected output window to have a namespace!'); + assertIsDefined(outputWindowNs, 'Expected output window to have a namespace!'); return outputWindowNs; } let ns = 'user'; @@ -72,7 +73,7 @@ export async function createNamespaceFromDocumentIfNotExists(doc) { const document = utilities.tryToGetDocument(doc); if (document) { const ns = getNamespace(document); - const client = replSession.getSession(utilities.getFileType(document)); + const client = replSession.tryToGetSession(utilities.getFileType(document)); if (client) { const nsList = await client.listNamespaces([]); if (nsList['ns-list'] && nsList['ns-list'].includes(ns)) { diff --git a/src/nrepl/bencode.ts b/src/nrepl/bencode.ts index b9cd4d133..f7ba62bea 100644 --- a/src/nrepl/bencode.ts +++ b/src/nrepl/bencode.ts @@ -5,6 +5,7 @@ */ import * as stream from 'stream'; import { Buffer } from 'buffer'; +import { assertIsDefined } from '../type-checks'; /** Bencode the given JSON object */ const bencode = (value) => { @@ -103,8 +104,9 @@ class BIncrementalDecoder { stack: State[] = []; private complete(data: any) { - if (this.stack.length) { - this.state = this.stack.pop(); + const state = this.stack.pop(); + if (state) { + this.state = state; if (this.state.id == 'list') { this.state.accum.push(data); this.stack.push(this.state); @@ -127,6 +129,7 @@ class BIncrementalDecoder { write(byte: number) { const ch = String.fromCharCode(byte); + let state: State | undefined; if (this.state.id == 'ready') { switch (ch) { case 'i': @@ -139,10 +142,9 @@ class BIncrementalDecoder { this.stack.push({ id: 'list', accum: [] }); break; case 'e': - if (!this.stack.length) { - throw 'unexpected end'; - } - this.state = this.stack.pop(); + state = this.stack.pop(); + assertIsDefined(state, 'Expected there to be states on the stack!'); + this.state = state; if (this.state.id == 'dict') { if (this.state.key !== null) { throw 'Missing value in dict'; diff --git a/src/nrepl/connectSequence.ts b/src/nrepl/connectSequence.ts index ba16f2b8d..953ab56e2 100644 --- a/src/nrepl/connectSequence.ts +++ b/src/nrepl/connectSequence.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as state from '../state'; import * as utilities from '../utilities'; import { getConfig } from '../config'; +import { assertIsDefined } from '../type-checks'; enum ProjectTypes { 'Leiningen' = 'Leiningen', @@ -252,7 +253,8 @@ const defaultCljsTypes: { [id: string]: CljsTypeConfig } = { /** Retrieve the replConnectSequences from the config */ function getCustomConnectSequences(): ReplConnectSequence[] { - const sequences: ReplConnectSequence[] = getConfig().replConnectSequences; + const sequences: ReplConnectSequence[] | undefined = getConfig().replConnectSequences; + assertIsDefined(sequences, 'Expected there to be repl connect sequences!'); for (const sequence of sequences) { if ( @@ -283,7 +285,7 @@ function getConnectSequences(projectTypes: string[]): ReplConnectSequence[] { const customSequences = getCustomConnectSequences(); const defSequences = projectTypes.reduce( (seqs, projectType) => seqs.concat(defaultSequences[projectType]), - [] + [] as ReplConnectSequence[] ); const defSequenceProjectTypes = [...new Set(defSequences.map((s) => s.projectType))]; const sequences = customSequences @@ -306,10 +308,10 @@ async function askForConnectSequence( cljTypes: string[], saveAs: string, logLabel: string -): Promise { +): Promise { // figure out what possible kinds of project we're in const sequences: ReplConnectSequence[] = getConnectSequences(cljTypes); - const projectRootUri = state.getProjectRootUri(); + const projectRootUri = state.tryToGetProjectRootUri(); const saveAsFull = projectRootUri ? `${projectRootUri.toString()}/${saveAs}` : saveAs; void state.extensionContext.workspaceState.update('askForConnectSequenceQuickPick', true); const projectConnectSequenceName = await utilities.quickPickSingle({ @@ -326,6 +328,7 @@ async function askForConnectSequence( return; } const sequence = sequences.find((seq) => seq.name === projectConnectSequenceName); + assertIsDefined(sequence, 'Expected to find a sequence!'); void state.extensionContext.workspaceState.update('selectedCljTypeName', sequence.projectType); return sequence; } diff --git a/src/nrepl/index.ts b/src/nrepl/index.ts index 9054d53f8..45e14933b 100644 --- a/src/nrepl/index.ts +++ b/src/nrepl/index.ts @@ -13,6 +13,7 @@ import type { ReplSessionType } from '../config'; import { getStateValue, prettyPrint } from '../../out/cljs-lib/cljs-lib'; import { getConfig } from '../config'; import { log, Direction, loggingEnabled } from './logging'; +import { omit } from 'lodash'; function hasStatus(res: any, status: string): boolean { return res.status && res.status.indexOf(status) > -1; @@ -207,7 +208,7 @@ export class NReplSession { } messageHandlers: { [id: string]: (msg: any) => boolean } = {}; - replType: ReplSessionType = null; + replType: ReplSessionType | null = null; close() { this.client.write({ op: 'close', session: this.sessionId }); @@ -359,11 +360,11 @@ export class NReplSession { ) { const pprintOptions = opts.pprintOptions; opts['pprint'] = pprintOptions.enabled; - delete opts.pprintOptions; + const optsWithoutPprint = omit(opts, 'pprintOptions'); const extraOpts = getServerSidePrinter(pprintOptions); const opMsg = this._createEvalOperationMessage(code, ns, { ...extraOpts, - ...opts, + ...optsWithoutPprint, }); const evaluation = new NReplEvaluation( @@ -381,6 +382,7 @@ export class NReplSession { if (evaluation.onMessage(msg, pprintOptions)) { return true; } + return false; }; this.addRunningID(opMsg.id); this.client.write(opMsg); @@ -433,7 +435,7 @@ export class NReplSession { this, opts.stderr, opts.stdout, - null, + undefined, new Promise((resolve, reject) => { this.messageHandlers[id] = (msg) => { evaluation.setHandlers(resolve, reject); @@ -441,6 +443,7 @@ export class NReplSession { if (evaluation.onMessage(msg, opts.pprintOptions)) { return true; } + return false; }; this.addRunningID(id); this.client.write({ @@ -616,6 +619,7 @@ export class NReplSession { resolve(res); return true; } + return false; }; this.client.write({ op: cmd, @@ -761,9 +765,9 @@ export class NReplEvaluation { constructor( public id: string, public session: NReplSession, - public stderr: (x: string) => void, - public stdout: (x: string) => void, - public stdin: () => Promise, + public stderr: ((x: string) => void) | undefined, + public stdout: ((x: string) => void) | undefined, + public stdin: (() => Promise) | undefined, public value: Promise ) {} diff --git a/src/nrepl/jack-in-terminal.ts b/src/nrepl/jack-in-terminal.ts index 2d175dee0..06ba96630 100644 --- a/src/nrepl/jack-in-terminal.ts +++ b/src/nrepl/jack-in-terminal.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as child from 'child_process'; import * as kill from 'tree-kill'; import * as outputWindow from '../results-output/results-doc'; +import { assertIsDefined } from '../type-checks'; export interface JackInTerminalOptions extends vscode.TerminalOptions { name: string; @@ -80,7 +81,7 @@ export class JackInTerminal implements vscode.Pseudoterminal { this.process.on('exit', (status) => { this.writeEmitter.fire(`Jack-in process exited. Status: ${status}\r\n`); }); - this.process.stdout.on('data', (data) => { + this.process.stdout?.on('data', (data) => { const msg = this.dataToString(data); this.writeEmitter.fire(`${msg}\r\n`); // Started nREPL server at 127.0.0.1:1337 @@ -89,9 +90,14 @@ export class JackInTerminal implements vscode.Pseudoterminal { // nbb - nRepl server started on port %d . nrepl-cljs-sci version %s 1337 TODO // TODO: Remove nbb WIP match if (msg.match(/Started nREPL server|nREPL server started/i)) { - const [_, port1, host1, host2, port2, port3] = msg.match( + const connectionInfo = msg.match( /(?:Started nREPL server|nREPL server started)[^\r\n]+?(?:(?:on port (\d+)(?: on host (\S+))?)|([^\s/]+):(\d+))|.*?(\d+) TODO/ ); + assertIsDefined( + connectionInfo, + 'Expected to find connection info in repl started messages!' + ); + const [_, port1, host1, host2, port2, port3] = connectionInfo; this.whenREPLStarted( this.process, host1 ? host1 : host2 ? host2 : 'localhost', @@ -99,7 +105,7 @@ export class JackInTerminal implements vscode.Pseudoterminal { ); } }); - this.process.stderr.on('data', (data) => { + this.process.stderr?.on('data', (data) => { const msg = this.dataToString(data); this.writeEmitter.fire(`${msg}\r\n`); }); @@ -111,8 +117,9 @@ export class JackInTerminal implements vscode.Pseudoterminal { if (this.process && !this.process.killed) { console.log('Closing any ongoing stdin event'); this.writeEmitter.fire('Killing the Jack-in process\r\n'); - this.process.stdin.end(() => { + this.process.stdin?.end(() => { console.log('Killing the Jack-in process'); + assertIsDefined(this.process.pid, 'Expected child process to have a pid!'); kill(this.process.pid); }); } else if (this.process && this.process.killed) { diff --git a/src/nrepl/jack-in.ts b/src/nrepl/jack-in.ts index 9e31429e2..e0ea58ff2 100644 --- a/src/nrepl/jack-in.ts +++ b/src/nrepl/jack-in.ts @@ -13,9 +13,10 @@ import * as outputWindow from '../results-output/results-doc'; import { JackInTerminal, JackInTerminalOptions, createCommandLine } from './jack-in-terminal'; import * as liveShareSupport from '../live-share'; import { getConfig } from '../config'; +import { assertIsDefined } from '../type-checks'; -let jackInPTY: JackInTerminal = undefined; -let jackInTerminal: vscode.Terminal = undefined; +let jackInPTY: JackInTerminal | undefined = undefined; +let jackInTerminal: vscode.Terminal | undefined = undefined; function cancelJackInTask() { setTimeout(() => { @@ -25,7 +26,7 @@ function cancelJackInTask() { function resolveEnvVariables(entry: any): any { if (typeof entry === 'string') { - const s = entry.replace(/\$\{env:(\w+)\}/, (_, v) => (process.env[v] ? process.env[v] : '')); + const s = entry.replace(/\$\{env:(\w+)\}/, (_, v) => process.env[v] || ''); return s; } else { return entry; @@ -84,7 +85,7 @@ function executeJackInTask( cancelJackInTask(); } ); - jackInTerminal = (vscode.window).createTerminal({ + jackInTerminal = vscode.window.createTerminal({ name: `Calva Jack-in: ${connectSequence.name}`, pty: jackInPTY, }); @@ -136,7 +137,7 @@ export async function copyJackInCommandToClipboard(): Promise { console.error('An error occurred while initializing project directory.', e); return; } - let projectConnectSequence: ReplConnectSequence; + let projectConnectSequence: ReplConnectSequence | undefined; try { projectConnectSequence = await getProjectConnectSequence(); } catch (e) { @@ -157,7 +158,7 @@ async function getJackInTerminalOptions( projectConnectSequence: ReplConnectSequence ): Promise { const projectTypeName: string = projectConnectSequence.projectType; - let selectedCljsType: CljsTypes; + let selectedCljsType: CljsTypes | undefined; if ( typeof projectConnectSequence.cljsType == 'string' && @@ -172,6 +173,9 @@ async function getJackInTerminalOptions( } const projectType = projectTypes.getProjectTypeForName(projectTypeName); + assertIsDefined(projectType, 'Expected to find a project type!'); + + const projectRootLocal = state.getProjectRootLocal(); let args: string[] = await projectType.commandLine(projectConnectSequence, selectedCljsType); let cmd: string[]; @@ -181,9 +185,7 @@ async function getJackInTerminalOptions( const jarSourceUri = vscode.Uri.file( path.join(state.extensionContext.extensionPath, 'deps.clj.jar') ); - const jarDestUri = vscode.Uri.file( - path.join(state.getProjectRootLocal(), '.calva', 'deps.clj.jar') - ); + const jarDestUri = vscode.Uri.file(path.join(projectRootLocal, '.calva', 'deps.clj.jar')); try { await vscode.workspace.fs.copy(jarSourceUri, jarDestUri, { overwrite: false, @@ -211,13 +213,13 @@ async function getJackInTerminalOptions( ...processEnvObject(projectConnectSequence.jackInEnv), }, isWin: projectTypes.isWin, - cwd: state.getProjectRootLocal(), + cwd: projectRootLocal, useShell: projectTypes.isWin ? projectType.processShellWin : projectType.processShellUnix, }; return terminalOptions; } -async function getProjectConnectSequence(): Promise { +async function getProjectConnectSequence(): Promise { const cljTypes: string[] = await projectTypes.detectProjectTypes(); if (cljTypes.length > 1) { const connectSequence = await askForConnectSequence( @@ -233,13 +235,14 @@ async function getProjectConnectSequence(): Promise { } } -export async function jackIn(connectSequence: ReplConnectSequence, cb?: () => unknown) { +export async function jackIn(connectSequence: ReplConnectSequence | undefined, cb?: () => unknown) { try { await liveShareSupport.setupLiveShareListener(); } catch (e) { console.error('An error occurred while setting up Live Share listener.', e); } - if (state.getProjectRootUri().scheme === 'vsls') { + const projectRootUri = state.getProjectRootUri(); + if (projectRootUri.scheme === 'vsls') { outputWindow.append("; Aborting Jack-in, since you're the guest of a live share session."); outputWindow.append( '; Please use this command instead: Connect to a running REPL server in the project.' @@ -251,7 +254,7 @@ export async function jackIn(connectSequence: ReplConnectSequence, cb?: () => un outputWindow.append('; Jacking in...'); await outputWindow.openResultsDoc(); - let projectConnectSequence: ReplConnectSequence = connectSequence; + let projectConnectSequence: ReplConnectSequence | undefined = connectSequence; if (!projectConnectSequence) { try { projectConnectSequence = await getProjectConnectSequence(); diff --git a/src/nrepl/project-types.ts b/src/nrepl/project-types.ts index fe546463c..2311c44fa 100644 --- a/src/nrepl/project-types.ts +++ b/src/nrepl/project-types.ts @@ -8,6 +8,7 @@ import { getConfig } from '../config'; import { keywordize, unKeywordize } from '../util/string'; import { CljsTypes, ReplConnectSequence } from './connectSequence'; import { parseForms, parseEdn } from '../../out/cljs-lib/cljs-lib'; +import { assertIsDefined } from '../type-checks'; export const isWin = /^win/.test(process.platform); @@ -20,8 +21,8 @@ export type ProjectType = { resolveBundledPathUnix?: () => string; processShellWin: boolean; processShellUnix: boolean; - commandLine: (connectSequence: ReplConnectSequence, cljsType: CljsTypes) => any; - useWhenExists: string; + commandLine: (connectSequence: ReplConnectSequence, cljsType: CljsTypes | undefined) => any; + useWhenExists: string | undefined; nReplPortFile: string[]; }; @@ -30,8 +31,14 @@ function nreplPortFileRelativePath(connectSequence: ReplConnectSequence): string if (connectSequence.nReplPortFile) { subPath = path.join(...connectSequence.nReplPortFile); } else { - const projectType: ProjectType | string = connectSequence.projectType; - subPath = path.join(...getProjectTypeForName(projectType).nReplPortFile); + const projectTypeName: ProjectType | string = connectSequence.projectType; + const projectType = getProjectTypeForName(projectTypeName); + assertIsDefined( + projectType, + `Expected a project type given project type name of ${projectTypeName}` + ); + + subPath = path.join(...projectType.nReplPortFile); } return subPath; } @@ -44,7 +51,7 @@ function nreplPortFileRelativePath(connectSequence: ReplConnectSequence): string */ export function nreplPortFileLocalPath(connectSequence: ReplConnectSequence): string { const relativePath = nreplPortFileRelativePath(connectSequence); - const projectRoot = state.getProjectRootLocal(); + const projectRoot = state.tryToGetProjectRootLocal(); if (projectRoot) { try { return path.resolve(projectRoot, relativePath); @@ -57,7 +64,7 @@ export function nreplPortFileLocalPath(connectSequence: ReplConnectSequence): st export function nreplPortFileUri(connectSequence: ReplConnectSequence): vscode.Uri { const relativePath = nreplPortFileRelativePath(connectSequence); - const projectRoot = state.getProjectRootUri(); + const projectRoot = state.tryToGetProjectRootUri(); if (projectRoot) { try { return vscode.Uri.joinPath(projectRoot, relativePath); @@ -85,7 +92,7 @@ export async function shadowBuilds(): Promise { ]; } -export function leinShadowBuilds(defproject: any): string[] { +export function leinShadowBuilds(defproject: any): string[] | undefined { if (defproject) { const shadowIndex = defproject.indexOf('shadow-cljs'); if (shadowIndex > -1) { @@ -108,18 +115,22 @@ export function leinShadowBuilds(defproject: any): string[] { async function selectShadowBuilds( connectSequence: ReplConnectSequence, - foundBuilds: string[] -): Promise<{ selectedBuilds: string[]; args: string[] }> { - const menuSelections = connectSequence.menuSelections, - selectedBuilds = menuSelections - ? menuSelections.cljsLaunchBuilds - : await utilities.quickPickMulti({ - values: foundBuilds.filter((x) => x[0] == ':'), - placeHolder: 'Select builds to start', - saveAs: `${state.getProjectRootUri().toString()}/shadow-cljs-jack-in`, - }), - aliases: string[] = - menuSelections && menuSelections.cljAliases ? menuSelections.cljAliases.map(keywordize) : []; // TODO do the same as clj to prompt the user with a list of aliases + foundBuilds: string[] | undefined +): Promise<{ selectedBuilds: string[] | undefined; args: string[] }> { + const menuSelections = connectSequence.menuSelections; + let selectedBuilds: string[] | undefined; + if (menuSelections) { + selectedBuilds = menuSelections.cljsLaunchBuilds; + } else { + assertIsDefined(foundBuilds, 'Expected to have foundBuilds when using the picker!'); + selectedBuilds = await utilities.quickPickMulti({ + values: foundBuilds.filter((x) => x[0] == ':'), + placeHolder: 'Select builds to start', + saveAs: `${state.getProjectRootUri().toString()}/shadow-cljs-jack-in`, + }); + } + const aliases: string[] = + menuSelections && menuSelections.cljAliases ? menuSelections.cljAliases.map(keywordize) : []; // TODO do the same as clj to prompt the user with a list of aliases const aliasesOption = aliases.length > 0 ? `-A${aliases.join('')}` : ''; const args: string[] = []; if (aliasesOption && aliasesOption.length) { @@ -145,9 +156,9 @@ async function leinDefProject(): Promise { async function leinProfilesAndAlias( defproject: any, connectSequence: ReplConnectSequence -): Promise<{ profiles: string[]; alias: string }> { +): Promise<{ profiles: string[]; alias: string | undefined }> { let profiles: string[] = [], - alias: string; + alias: string | undefined; if (defproject) { const aliasesIndex = defproject.indexOf('aliases'); @@ -157,7 +168,7 @@ async function leinProfilesAndAlias( leinAlias = menuSelections ? menuSelections.leinAlias : undefined; if (leinAlias) { alias = unKeywordize(leinAlias); - } else if (leinAlias === null) { + } else if (leinAlias === undefined) { alias = undefined; } else { let aliases: string[] = []; @@ -170,7 +181,7 @@ async function leinProfilesAndAlias( saveAs: `${state.getProjectRootUri().toString()}/lein-cli-alias`, placeHolder: 'Choose alias to launch with', }); - alias = alias == 'No alias' ? undefined : alias; + alias = alias === 'No alias' ? undefined : alias; } } } catch (error) { @@ -211,9 +222,15 @@ export enum JackInDependency { 'cider/piggieback' = 'cider/piggieback', } -const NREPL_VERSION = () => getConfig().jackInDependencyVersions['nrepl'], - CIDER_NREPL_VERSION = () => getConfig().jackInDependencyVersions['cider-nrepl'], - PIGGIEBACK_VERSION = () => getConfig().jackInDependencyVersions['cider/piggieback']; +const jackInDependencyVersions = getConfig().jackInDependencyVersions; +assertIsDefined( + jackInDependencyVersions, + 'Expected jackInDependencyVersions to be set in the config!' +); + +const NREPL_VERSION = () => jackInDependencyVersions['nrepl'], + CIDER_NREPL_VERSION = () => jackInDependencyVersions['cider-nrepl'], + PIGGIEBACK_VERSION = () => jackInDependencyVersions['cider/piggieback']; const cliDependencies = () => { return { @@ -386,11 +403,9 @@ const projectTypes: { [id: string]: ProjectType } = { const chan = state.outputChannel(); const defproject = await leinDefProject(); - const foundBuilds = leinShadowBuilds(defproject), - { selectedBuilds, args: extraArgs } = await selectShadowBuilds( - connectSequence, - foundBuilds - ); + const foundBuilds = leinShadowBuilds(defproject); + + const { selectedBuilds } = await selectShadowBuilds(connectSequence, foundBuilds); if (selectedBuilds && selectedBuilds.length) { return leinCommandLine(['shadow', 'watch', ...selectedBuilds], cljsType, connectSequence); @@ -445,19 +460,25 @@ const projectTypes: { [id: string]: ProjectType } = { }, }; -async function cljCommandLine(connectSequence: ReplConnectSequence, cljsType: CljsTypes) { +async function cljCommandLine( + connectSequence: ReplConnectSequence, + cljsType: CljsTypes | undefined +) { const out: string[] = []; let depsUri: vscode.Uri; + + const projectRootUri = state.getProjectRootUri(); + try { - depsUri = vscode.Uri.joinPath(state.getProjectRootUri(), 'deps.edn'); + depsUri = vscode.Uri.joinPath(projectRootUri, 'deps.edn'); } catch { - depsUri = vscode.Uri.file(path.join(state.getProjectRootUri().fsPath, 'deps.edn')); + depsUri = vscode.Uri.file(path.join(projectRootUri.fsPath, 'deps.edn')); } let parsed; if (connectSequence.projectType !== 'generic') { void vscode.workspace.fs.stat(depsUri); const bytes = await vscode.workspace.fs.readFile( - vscode.Uri.joinPath(state.getProjectRootUri(), 'deps.edn') + vscode.Uri.joinPath(projectRootUri, 'deps.edn') ); const data = new TextDecoder('utf-8').decode(bytes); try { @@ -510,7 +531,7 @@ async function cljCommandLine(connectSequence: ReplConnectSequence, cljsType: Cl if (projectAliases.length) { aliases = await utilities.quickPickMulti({ values: projectAliases.map(keywordize), - saveAs: `${state.getProjectRootUri().toString()}/clj-cli-aliases`, + saveAs: `${projectRootUri.toString()}/clj-cli-aliases`, placeHolder: 'Pick any aliases to launch with', }); } @@ -552,7 +573,7 @@ async function cljCommandLine(connectSequence: ReplConnectSequence, cljsType: Cl async function leinCommandLine( command: string[], - cljsType: CljsTypes, + cljsType: CljsTypes | undefined, connectSequence: ReplConnectSequence ) { const out: string[] = []; @@ -616,13 +637,13 @@ export function getProjectTypeForName(name: string) { } export async function detectProjectTypes(): Promise { - const rootUri = state.getProjectRootUri(); const cljProjTypes = ['generic', 'cljs-only', 'babashka', 'nbb']; for (const clj in projectTypes) { if (projectTypes[clj].useWhenExists) { try { const projectFileName = projectTypes[clj].useWhenExists; - const uri = vscode.Uri.joinPath(rootUri, projectFileName); + assertIsDefined(projectFileName, 'Expected there to be a project filename!'); + const uri = vscode.Uri.joinPath(state.getProjectRootUri(), projectFileName); await vscode.workspace.fs.readFile(uri); cljProjTypes.push(clj); } catch { diff --git a/src/nrepl/repl-session.ts b/src/nrepl/repl-session.ts index 6c488b6e9..fbcb93486 100644 --- a/src/nrepl/repl-session.ts +++ b/src/nrepl/repl-session.ts @@ -2,8 +2,9 @@ import { NReplSession } from '.'; import { cljsLib, tryToGetDocument, getFileType } from '../utilities'; import * as outputWindow from '../results-output/results-doc'; import { isUndefined } from 'lodash'; +import { assertIsDefined } from '../type-checks'; -function getSession(fileType?: string): NReplSession { +function tryToGetSession(fileType?: string): NReplSession | undefined { const doc = tryToGetDocument({}); if (isUndefined(fileType)) { @@ -20,6 +21,14 @@ function getSession(fileType?: string): NReplSession { } } +function getSession(fileType?: string): NReplSession { + const session = tryToGetSession(fileType); + + assertIsDefined(session, 'Expected to be able to get an nrepl session!'); + + return session; +} + function getReplSessionType(connected: boolean): string | undefined { const doc = tryToGetDocument({}); const fileType = getFileType(doc); @@ -28,12 +37,12 @@ function getReplSessionType(connected: boolean): string | undefined { if (connected) { if (outputWindow.isResultsDoc(doc)) { sessionType = outputWindow.getSessionType(); - } else if (fileType == 'cljs' && getSession('cljs') !== null) { + } else if (fileType == 'cljs' && tryToGetSession('cljs') !== undefined) { sessionType = 'cljs'; - } else if (fileType == 'clj' && getSession('clj') !== null) { + } else if (fileType == 'clj' && tryToGetSession('clj') !== undefined) { sessionType = 'clj'; - } else if (getSession('cljc') !== null) { - sessionType = getSession('cljc') == getSession('clj') ? 'clj' : 'cljs'; + } else if (tryToGetSession('cljc') !== undefined) { + sessionType = tryToGetSession('cljc') == tryToGetSession('clj') ? 'clj' : 'cljs'; } else { sessionType = 'clj'; } @@ -48,8 +57,14 @@ function updateReplSessionType() { cljsLib.setStateValue('current-session-type', replSessionType); } -function getReplSessionTypeFromState() { +function getReplSessionTypeFromState(): string | undefined { return cljsLib.getStateValue('current-session-type'); } -export { getSession, getReplSessionType, updateReplSessionType, getReplSessionTypeFromState }; +export { + tryToGetSession, + getSession, + getReplSessionType, + updateReplSessionType, + getReplSessionTypeFromState, +}; diff --git a/src/nrepl/repl-start.ts b/src/nrepl/repl-start.ts index deb87e815..a8e86e114 100644 --- a/src/nrepl/repl-start.ts +++ b/src/nrepl/repl-start.ts @@ -12,6 +12,7 @@ import * as cljsLib from '../../out/cljs-lib/cljs-lib'; import { ReplConnectSequence } from './connectSequence'; import * as clojureLsp from '../lsp/main'; import * as calvaConfig from '../config'; +import { assertIsDefined } from '../type-checks'; const TEMPLATES_SUB_DIR = 'bundled'; const DRAM_BASE_URL = 'https://raw.githubusercontent.com/BetterThanTomorrow/dram'; @@ -61,7 +62,7 @@ async function fetchConfig(configName: string): Promise { async function downloadDram(storageUri: vscode.Uri, configPath: string, filePath: string) { const calva = vscode.extensions.getExtension('betterthantomorrow.calva'); - const calvaVersion = calva.packageJSON.version; + const calvaVersion = calva?.packageJSON.version; const isDebug = process.env['IS_DEBUG'] === 'true'; const branch = isDebug || calvaVersion.match(/-.+$/) ? 'dev' : 'published'; const dramBaseUrl = `${DRAM_BASE_URL}/${branch}/drams`; @@ -167,7 +168,9 @@ export async function startStandaloneRepl( void clojureLsp.startClientCommand(); } - const [mainDoc, mainEditor] = await openStoredDoc(storageUri, tempDirUri, config.files[0]); + const main = await openStoredDoc(storageUri, tempDirUri, config.files[0]); + assertIsDefined(main, 'Expected to be able to open the stored doc!'); + const [mainDoc, mainEditor] = main; for (const file of config.files.slice(1)) { await openStoredDoc(storageUri, tempDirUri, file); } @@ -186,7 +189,9 @@ export async function startStandaloneRepl( viewColumn: vscode.ViewColumn.One, preserveFocus: false, }); - await eval.loadFile({}, getConfig().prettyPrintingOptions); + const pprintOptions = getConfig().prettyPrintingOptions; + assertIsDefined(pprintOptions, 'Expected there to be pretty printing options configured!'); + await eval.loadFile({}, pprintOptions); outputWindow.appendPrompt(); }); } @@ -242,7 +247,7 @@ export function startOrConnectRepl() { commands[START_HELLO_CLJS_NODE_OPTION] = START_HELLO_CLJS_NODE_COMMAND; } else { commands[DISCONNECT_OPTION] = DISCONNECT_COMMAND; - if (replSession.getSession('clj')) { + if (replSession.tryToGetSession('clj')) { commands[OPEN_WINDOW_OPTION] = OPEN_WINDOW_COMMAND; } } diff --git a/src/paredit/extension.ts b/src/paredit/extension.ts index e136c7fce..b6dd74403 100644 --- a/src/paredit/extension.ts +++ b/src/paredit/extension.ts @@ -13,7 +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'; +import { assertIsDefined } from '../type-checks'; const onPareditKeyMapChangedEmitter = new EventEmitter(); @@ -109,7 +109,9 @@ const pareditCommands: PareditCommand[] = [ { command: 'paredit.rangeForDefun', handler: (doc: EditableDocument) => { - paredit.selectRange(doc, paredit.rangeForDefun(doc)); + const range = paredit.rangeForDefun(doc); + assertIsDefined(range, 'Expected to find a range for the current defun!'); + paredit.selectRange(doc, range); }, }, { diff --git a/src/printer.ts b/src/printer.ts index db5d5594a..8af4d2fb7 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -1,5 +1,5 @@ import { getConfig } from './config'; -import { assertIsDefined } from './utilities'; +import { assertIsDefined } from './type-checks'; export type PrintFnOptions = { name: string; diff --git a/src/project-root.ts b/src/project-root.ts index 01b5369a2..55b0ffd9f 100644 --- a/src/project-root.ts +++ b/src/project-root.ts @@ -15,8 +15,9 @@ export async function findProjectRootPaths() { const excludeDirsGlob = excludePattern(); const t0 = new Date().getTime(); const rootPaths: string[] = []; - if (vscode.workspace.workspaceFolders?.length > 0) { - const wsRootPaths = vscode.workspace.workspaceFolders.map((f) => f.uri.fsPath); + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const wsRootPaths = workspaceFolders.map((f) => f.uri.fsPath); rootPaths.push(...wsRootPaths); } const candidateUris = await vscode.workspace.findFiles(projectFilesGlob, excludeDirsGlob, 10000); diff --git a/src/providers/annotations.ts b/src/providers/annotations.ts index 86ac02385..0024893c7 100644 --- a/src/providers/annotations.ts +++ b/src/providers/annotations.ts @@ -27,7 +27,7 @@ const evalResultsDecorationType = vscode.window.createTextEditorDecorationType({ rangeBehavior: vscode.DecorationRangeBehavior.ClosedOpen, }); -let resultsLocations: [vscode.Range, vscode.Position, vscode.Location][] = []; +let resultsLocations: [vscode.Range, vscode.Position | undefined, vscode.Location][] = []; function getResultsLocation(pos: vscode.Position): vscode.Location | undefined { for (const [range, _evaluatePosition, location] of resultsLocations) { @@ -141,7 +141,7 @@ function decorateSelection( resultString: string, codeSelection: vscode.Selection, editor: vscode.TextEditor, - evaluatePosition: vscode.Position, + evaluatePosition: vscode.Position | undefined, resultsLocation, status: AnnotationStatus ) { diff --git a/src/providers/completion.ts b/src/providers/completion.ts index 45b21d150..367ace46f 100644 --- a/src/providers/completion.ts +++ b/src/providers/completion.ts @@ -3,12 +3,11 @@ import { Position, CancellationToken, CompletionContext, - Hover, CompletionItemKind, window, CompletionList, CompletionItemProvider, - CompletionItem, + CompletionItem as VSCompletionItem, CompletionItemLabel, } from 'vscode'; import * as util from '../utilities'; @@ -20,6 +19,8 @@ import * as replSession from '../nrepl/repl-session'; import { getClient } from '../lsp/main'; import { CompletionRequest, CompletionResolveRequest } from 'vscode-languageserver-protocol'; import { createConverter } from 'vscode-languageclient/lib/common/protocolConverter'; +import { CompletionItem as LSPCompletionItem } from 'vscode-languageclient'; +import { assertIsDefined } from '../type-checks'; const mappings = { nil: CompletionItemKind.Value, @@ -35,7 +36,10 @@ const mappings = { const converter = createConverter(undefined, undefined); -const completionProviderOptions = { priority: ['lsp', 'repl'], merge: true }; +const completionProviderOptions: { priority: ['lsp', 'repl']; merge: boolean } = { + priority: ['lsp', 'repl'], + merge: true, +}; const completionFunctions = { lsp: lspCompletions, repl: replCompletions }; @@ -45,25 +49,39 @@ async function provideCompletionItems( token: CancellationToken, context: CompletionContext ) { - let results = []; + let results: (VSCompletionItem | LSPCompletionItem)[] = []; for (const provider of completionProviderOptions.priority) { if (results.length && !completionProviderOptions.merge) { break; } - const completions = await completionFunctions[provider](document, position, token, context); - - if (completions) { - results = [ - ...completions - .concat(results) - .reduce( - (m: Map, o: CompletionItem) => - m.set(o.label, Object.assign(m.get(o.label) || {}, o)), - new Map() - ) - .values(), - ]; + + const completionResult = await completionFunctions[provider]( + document, + position, + token, + context + ); + + if (completionResult === null) { + continue; } + + const completions: (VSCompletionItem | LSPCompletionItem)[] = Array.isArray(completionResult) + ? completionResult + : completionResult.items; + + results = [ + ...completions + .concat(results) + .reduce( + ( + m: Map, + o: VSCompletionItem | LSPCompletionItem + ) => m.set(o.label, Object.assign(m.get(o.label) || {}, o)), + new Map() + ) + .values(), + ]; } return new CompletionList(results.map(converter.asCompletionItem), true); @@ -79,13 +97,13 @@ export default class CalvaCompletionItemProvider implements CompletionItemProvid return provideCompletionItems(document, position, token, context); } - async resolveCompletionItem(item: CompletionItem, token: CancellationToken) { + async resolveCompletionItem(item: VSCompletionItem, token: CancellationToken) { if (util.getConnectedState() && item['data']?.provider === 'repl') { const activeTextEditor = window.activeTextEditor; - util.assertIsDefined(activeTextEditor, 'Expected window to have activeTextEditor defined!'); + assertIsDefined(activeTextEditor, 'Expected window to have activeTextEditor defined!'); - const client = replSession.getSession(util.getFileType(activeTextEditor.document)); + const client = replSession.tryToGetSession(util.getFileType(activeTextEditor.document)); if (client) { await namespace.createNamespaceFromDocumentIfNotExists(activeTextEditor.document); const ns = namespace.getDocumentNamespace(); @@ -121,7 +139,7 @@ async function lspCompletions( ); } -async function lspResolveCompletions(item: CompletionItem, token: CancellationToken) { +async function lspResolveCompletions(item: VSCompletionItem, token: CancellationToken) { const lspClient = await getClient(20); return lspClient.sendRequest( CompletionResolveRequest.type, @@ -135,7 +153,7 @@ async function replCompletions( position: Position, _token: CancellationToken, _context: CompletionContext -): Promise { +): Promise { if (!util.getConnectedState()) { return []; } @@ -143,14 +161,14 @@ async function replCompletions( const toplevelSelection = select.getFormSelection(document, position, true); - util.assertIsDefined(toplevelSelection, 'Expected a topLevelSelection!'); + assertIsDefined(toplevelSelection, 'Expected a topLevelSelection!'); const toplevel = document.getText(toplevelSelection), toplevelStartOffset = document.offsetAt(toplevelSelection.start), toplevelStartCursor = docMirror.getDocument(document).getTokenCursor(toplevelStartOffset + 1), wordRange = document.getWordRangeAtPosition(position); - util.assertIsDefined(wordRange, 'Expected a wordRange!'); + assertIsDefined(wordRange, 'Expected a wordRange!'); const wordStartLocalOffset = document.offsetAt(wordRange.start) - toplevelStartOffset, wordEndLocalOffset = document.offsetAt(wordRange.end) - toplevelStartOffset, @@ -159,8 +177,11 @@ async function replCompletions( replContext = `${contextStart}__prefix__${contextEnd}`, toplevelIsValidForm = toplevelStartCursor.withinValidList() && replContext != '__prefix__', ns = namespace.getNamespace(document), - client = replSession.getSession(util.getFileType(document)), - res = await client.complete(ns, text, toplevelIsValidForm ? replContext : undefined), + client = replSession.tryToGetSession(util.getFileType(document)); + + assertIsDefined(client, 'Expected there to be a repl client!'); + + const res = await client.complete(ns, text, toplevelIsValidForm ? replContext : undefined), results = res.completions || []; results.forEach((element) => { @@ -171,7 +192,7 @@ async function replCompletions( } }); return results.map((item) => { - const result = new CompletionItem( + const result = new VSCompletionItem( item.candidate, mappings[item.type] || CompletionItemKind.Text ); diff --git a/src/providers/hover.ts b/src/providers/hover.ts index 748781b1f..05d5997e0 100644 --- a/src/providers/hover.ts +++ b/src/providers/hover.ts @@ -13,7 +13,7 @@ export async function provideHover(document: vscode.TextDocument, position: vsco if (util.getConnectedState()) { const text = util.getWordAtPosition(document, position); const ns = namespace.getNamespace(document); - const client = replSession.getSession(util.getFileType(document)); + const client = replSession.tryToGetSession(util.getFileType(document)); if (client) { await namespace.createNamespaceFromDocumentIfNotExists(document); const res = await client.info(ns, text); diff --git a/src/providers/infoparser.ts b/src/providers/infoparser.ts index fd27a266a..fdf607b82 100644 --- a/src/providers/infoparser.ts +++ b/src/providers/infoparser.ts @@ -1,6 +1,7 @@ import { SignatureInformation, ParameterInformation, MarkdownString } from 'vscode'; import * as tokenCursor from '../cursor-doc/token-cursor'; import { getConfig } from '../config'; +import { isString } from 'lodash'; export type Completion = | [string, string] @@ -10,8 +11,16 @@ export type Completion = export class REPLInfoParser { private _name: string | undefined = undefined; + /* + * Different arities of arglists for a symbol. + * e.g. "[]" or "[xs]" or "[s re]\n[s re limit]" + */ private _arglist: string | undefined = undefined; + /* + * Different forms a special form can take. + * e.g. "(do exprs*)" or "(Classname. args*)\n(new Classname args*)" + */ private _formsString: string | undefined = undefined; private _docString: string | undefined = undefined; @@ -99,7 +108,7 @@ export class REPLInfoParser { getHover(): MarkdownString { const hover = new MarkdownString(); - if (this._name !== '') { + if (isString(this._name) && this._name !== '') { if (!this._specialForm || this._isMacro) { hover.appendCodeblock(this._name, 'clojure'); if (this._arglist) { @@ -117,7 +126,10 @@ export class REPLInfoParser { } else { hover.appendText('\n'); } - hover.appendMarkdown(this._docString); + + if (isString(this._docString)) { + hover.appendMarkdown(this._docString); + } } return hover; } @@ -145,27 +157,33 @@ export class REPLInfoParser { } getSignatures(symbol: string): SignatureInformation[] | undefined { - if (this._name !== '') { + if (isString(this._name) && this._name !== '') { const argLists = this._arglist ? this._arglist : this._formsString; - if (argLists) { - return argLists + if (isString(argLists) && argLists !== '') { + // Break down arglist or formsString into the different arglists/arities, + // removing empty strings since those make no sense as arglists. + const argListStrings = argLists .split('\n') - .map((argList) => argList.trim()) - .map((argList) => { - if (argList !== '') { - const signature = new SignatureInformation(`(${symbol} ${argList})`); - // Skip parameter help on special forms and forms with optional arguments, for now - if (this._arglist && !argList.match(/\?/)) { - signature.parameters = this.getParameters(symbol, argList); - } - if (this._docString && getConfig().showDocstringInParameterHelp) { - signature.documentation = new MarkdownString(this._docString); - } - return signature; - } else { - return undefined; + .map((a) => a.trim()) + .filter((a) => a !== ''); + + const signatures = argListStrings.map((argList) => { + const signature = new SignatureInformation(`(${symbol} ${argList})`); + // Skip parameter help on special forms and forms with optional arguments, for now + if (this._arglist && !argList.includes('?')) { + const parameters = this.getParameters(symbol, argList); + + if (parameters) { + signature.parameters = parameters; } - }); + } + if (this._docString && getConfig().showDocstringInParameterHelp) { + signature.documentation = new MarkdownString(this._docString); + } + return signature; + }); + + return signatures; } } return undefined; diff --git a/src/providers/signature.ts b/src/providers/signature.ts index fc15cbbe0..cad344326 100644 --- a/src/providers/signature.ts +++ b/src/providers/signature.ts @@ -13,6 +13,7 @@ import { LispTokenCursor } from '../cursor-doc/token-cursor'; import * as docMirror from '../doc-mirror/index'; import * as namespace from '../namespace'; import * as replSession from '../nrepl/repl-session'; +import { assertIsDefined } from '../type-checks'; export class CalvaSignatureHelpProvider implements SignatureHelpProvider { async provideSignatureHelp( @@ -34,7 +35,7 @@ export async function provideSignatureHelp( idx = document.offsetAt(position), symbol = getSymbol(document, idx); if (symbol) { - const client = replSession.getSession(util.getFileType(document)); + const client = replSession.tryToGetSession(util.getFileType(document)); if (client) { await namespace.createNamespaceFromDocumentIfNotExists(document); const res = await client.info(ns, symbol), @@ -42,12 +43,13 @@ export async function provideSignatureHelp( if (signatures) { const help = new SignatureHelp(), currentArgsRanges = getCurrentArgsRanges(document, idx); + assertIsDefined(currentArgsRanges, 'Expected to find the current args ranges!'); help.signatures = signatures; help.activeSignature = getActiveSignatureIdx(signatures, currentArgsRanges.length); if (signatures[help.activeSignature].parameters !== undefined) { const currentArgIdx = currentArgsRanges.findIndex((range) => range.contains(position)), activeSignature = signatures[help.activeSignature]; - util.assertIsDefined(activeSignature, 'Expected activeSignature to be defined!'); + assertIsDefined(activeSignature, 'Expected activeSignature to be defined!'); help.activeParameter = activeSignature.label.match(/&/) !== null ? Math.min(currentArgIdx, activeSignature.parameters.length - 1) @@ -68,7 +70,7 @@ function getCurrentArgsRanges(document: TextDocument, idx: number): Range[] | un // Are we in a function that gets a threaded first parameter? const { previousRangeIndex, previousFunction } = getPreviousRangeIndexAndFunction(document, idx); const isInThreadFirst: boolean = - (previousRangeIndex > 1 && ['->', 'some->'].includes(previousFunction)) || + (previousRangeIndex > 1 && previousFunction && ['->', 'some->'].includes(previousFunction)) || (previousRangeIndex > 1 && previousRangeIndex % 2 !== 0 && previousFunction === 'cond->'); if (allRanges !== undefined) { @@ -83,7 +85,7 @@ function getActiveSignatureIdx(signatures: SignatureInformation[], currentArgsCo return activeSignatureIdx !== -1 ? activeSignatureIdx : signatures.length - 1; } -function getSymbol(document: TextDocument, idx: number): string { +function getSymbol(document: TextDocument, idx: number): string | undefined { const cursor: LispTokenCursor = docMirror.getDocument(document).getTokenCursor(idx); return cursor.getFunctionName(); } diff --git a/src/refresh.ts b/src/refresh.ts index 36b03e4f6..191624120 100644 --- a/src/refresh.ts +++ b/src/refresh.ts @@ -22,10 +22,10 @@ function report(res, chan: vscode.OutputChannel) { function refresh(document = {}) { const doc = util.tryToGetDocument(document), - client: NReplSession = replSession.getSession(util.getFileType(doc)), + client: NReplSession | undefined = replSession.tryToGetSession(util.getFileType(doc)), chan: vscode.OutputChannel = state.outputChannel(); - if (client != undefined) { + if (client !== undefined) { chan.appendLine('Reloading...'); void client.refresh().then((res) => { report(res, chan); @@ -37,10 +37,10 @@ function refresh(document = {}) { function refreshAll(document = {}) { const doc = util.tryToGetDocument(document), - client: NReplSession = replSession.getSession(util.getFileType(doc)), + client: NReplSession | undefined = replSession.tryToGetSession(util.getFileType(doc)), chan: vscode.OutputChannel = state.outputChannel(); - if (client != undefined) { + if (client !== undefined) { chan.appendLine('Reloading all the things...'); void client.refreshAll().then((res) => { report(res, chan); diff --git a/src/results-output/repl-history.ts b/src/results-output/repl-history.ts index bd0f2d4af..5b2399b36 100644 --- a/src/results-output/repl-history.ts +++ b/src/results-output/repl-history.ts @@ -9,12 +9,13 @@ import type { ReplSessionType } from '../config'; import { isResultsDoc, getSessionType, getPrompt, append } from './results-doc'; import { addToHistory } from './util'; import { isUndefined } from 'lodash'; +import { assertIsDefined } from '../type-checks'; const replHistoryCommandsActiveContext = 'calva:replHistoryCommandsActive'; let historyIndex: number | undefined = undefined; let lastTextAtPrompt: string | undefined = undefined; -function setReplHistoryCommandsActiveContext(editor: vscode.TextEditor): void { +function setReplHistoryCommandsActiveContext(editor: vscode.TextEditor | undefined): void { if (editor && util.getConnectedState() && isResultsDoc(editor.document)) { const document = editor.document; const selection = editor.selection; @@ -113,7 +114,7 @@ function showPreviousReplHistoryEntry(): void { historyIndex = history.length; lastTextAtPrompt = textAtPrompt; } else { - util.assertIsDefined(textAtPrompt, 'Expected to find text at the prompt!'); + assertIsDefined(textAtPrompt, 'Expected to find text at the prompt!'); updateReplHistory(replSessionType, history, textAtPrompt, historyIndex); } historyIndex--; @@ -125,7 +126,7 @@ function showNextReplHistoryEntry(): void { const doc = editor.document; const replSessionType = getSessionType(); const history = getHistory(replSessionType); - if (!isResultsDoc(doc) || historyIndex === null) { + if (!isResultsDoc(doc) || historyIndex === undefined) { return; } if (historyIndex === history.length - 1) { @@ -133,8 +134,8 @@ function showNextReplHistoryEntry(): void { 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!'); + assertIsDefined(textAtPrompt, 'Expected to find text at the prompt!'); + 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 144e12c05..1472401db 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -14,6 +14,7 @@ import * as docMirror from '../doc-mirror/index'; import { PrintStackTraceCodelensProvider } from '../providers/codelense'; import * as replSession from '../nrepl/repl-session'; import { splitEditQueueForTextBatching } from './util'; +import { assertIsDefined, isDefined } from '../type-checks'; const RESULTS_DOC_NAME = `output.${config.REPL_FILE_EXT}`; @@ -42,7 +43,6 @@ 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 { @@ -88,7 +88,7 @@ export function getSession(): NReplSession | undefined { return _sessionInfo[_sessionType].session; } -export function setSession(session: NReplSession, newNs?: string): void { +export function setSession(session: NReplSession | undefined, newNs?: string): void { if (session) { if (session.replType) { _sessionType = session.replType; @@ -177,7 +177,7 @@ export async function initResultsDoc(): Promise { promptCursor.previous(); } while (promptCursor.getPrevToken().type !== 'prompt' && !promptCursor.atStart()); const submitRange = selectionCursor.rangeForCurrentForm(idx); - submitOnEnter = submitRange && submitRange[1] > promptCursor.offsetStart; + submitOnEnter = !!submitRange && submitRange[1] > promptCursor.offsetStart; } } } @@ -228,7 +228,7 @@ export async function revealDocForCurrentNS(preserveFocus: boolean = true) { export async function setNamespaceFromCurrentFile() { const session = replSession.getSession(); const ns = namespace.getNamespace(util.tryToGetDocument({})); - if (getNs() !== ns && util.isDefined(ns)) { + if (getNs() !== ns && isDefined(ns)) { await session.switchNS(ns); } setSession(session, ns); @@ -338,7 +338,8 @@ async function writeNextOutputBatch() { // Any entries that contain onAppended are not batched with other pending // entries to simplify providing the correct insert position to the callback. if (resultsBuffer[0].onAppended) { - return await writeToResultsDoc(resultsBuffer.shift()); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return await writeToResultsDoc(resultsBuffer.shift()!); } // Batch all remaining entries up until another onAppended callback. const [nextText, remaining] = splitEditQueueForTextBatching(resultsBuffer); @@ -433,5 +434,9 @@ export function appendPrompt(onAppended?: OnAppendedCallback) { } function getUriForCurrentNamespace(): Promise { - return namespace.getUriForNamespace(getSession(), getNs()); + const session = getSession(); + assertIsDefined(session, 'Expected there to be a current session!'); + const ns = getNs(); + assertIsDefined(ns, 'Expected there to be a current namespace!'); + return namespace.getUriForNamespace(session, ns); } diff --git a/src/state.ts b/src/state.ts index 3483d1002..e2573a614 100644 --- a/src/state.ts +++ b/src/state.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as os from 'os'; import { getStateValue, setStateValue } from '../out/cljs-lib/cljs-lib'; import * as projectRoot from './project-root'; +import { assertIsDefined } from './type-checks'; let extensionContext: vscode.ExtensionContext; export function setExtensionContext(context: vscode.ExtensionContext) { @@ -46,12 +47,20 @@ const PROJECT_DIR_KEY = 'connect.projectDir'; const PROJECT_DIR_URI_KEY = 'connect.projectDirNew'; const PROJECT_CONFIG_MAP = 'config'; -export function getProjectRootLocal(useCache = true): string | undefined { +export function tryToGetProjectRootLocal(useCache = true): string | undefined { if (useCache) { return getStateValue(PROJECT_DIR_KEY); } } +export const getProjectRootLocal = (useCache = true): string => { + const projectRootLocal = tryToGetProjectRootLocal(useCache); + + assertIsDefined(projectRootLocal, 'Expected to find a local project root!'); + + return projectRootLocal; +}; + export function getProjectConfig(useCache = true) { if (useCache) { return getStateValue(PROJECT_CONFIG_MAP); @@ -62,11 +71,18 @@ export function setProjectConfig(config) { return setStateValue(PROJECT_CONFIG_MAP, config); } -export function getProjectRootUri(useCache = true): vscode.Uri | undefined { +export function tryToGetProjectRootUri(useCache = true): vscode.Uri | undefined { if (useCache) { return getStateValue(PROJECT_DIR_URI_KEY); } } +export function getProjectRootUri(useCache = true): vscode.Uri { + const projectRootUri = tryToGetProjectRootUri(useCache); + + assertIsDefined(projectRootUri, 'Expected to find project root URI!'); + + return projectRootUri; +} const NON_PROJECT_DIR_KEY = 'calva.connect.nonProjectDir'; @@ -104,7 +120,7 @@ export async function setOrCreateNonProjectRoot( ): Promise { let root: vscode.Uri | undefined = undefined; if (preferProjectDir) { - root = getProjectRootUri(); + root = tryToGetProjectRootUri(); } if (!root) { root = await getNonProjectRootDir(context); diff --git a/src/statusbar.ts b/src/statusbar.ts index 347d610eb..5a3a12c99 100644 --- a/src/statusbar.ts +++ b/src/statusbar.ts @@ -4,7 +4,8 @@ import * as util from './utilities'; import * as config from './config'; import status from './status'; import { getStateValue } from '../out/cljs-lib/cljs-lib'; -import { getSession, getReplSessionTypeFromState } from './nrepl/repl-session'; +import { tryToGetSession, getReplSessionTypeFromState } from './nrepl/repl-session'; +import { assertIsDefined } from './type-checks'; const connectionStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); const typeStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1); @@ -24,7 +25,7 @@ const color = { function colorValue(section: string, currentConf: vscode.WorkspaceConfiguration): string { const configSection = currentConf.inspect(section); - util.assertIsDefined(configSection, () => `Expected config section "${section}" to be defined!`); + assertIsDefined(configSection, () => `Expected config section "${section}" to be defined!`); const { defaultValue, globalValue, workspaceFolderValue, workspaceValue } = configSection; @@ -83,11 +84,11 @@ function update(context = state.extensionContext) { connectionStatus.command = 'calva.startOrConnectRepl'; typeStatus.color = colorValue('typeStatusColor', currentConf); const replType = getReplSessionTypeFromState(); - if (replType !== null) { + if (replType !== undefined) { typeStatus.text = ['cljc', config.REPL_FILE_EXT].includes(fileType) ? `cljc/${replType}` : replType; - if (getSession('clj') !== null && getSession('cljs') !== null) { + if (tryToGetSession('clj') !== undefined && tryToGetSession('cljs') !== undefined) { typeStatus.command = 'calva.toggleCLJCSession'; typeStatus.tooltip = `Click to use ${replType === 'clj' ? 'cljs' : 'clj'} REPL for cljc`; } else { diff --git a/src/testRunner.ts b/src/testRunner.ts index 0642e0070..abbb36cae 100644 --- a/src/testRunner.ts +++ b/src/testRunner.ts @@ -9,6 +9,7 @@ import * as lsp from './lsp/types'; import * as namespace from './namespace'; import { getSession, updateReplSessionType } from './nrepl/repl-session'; import * as getText from './util/get-text'; +import { assertIsDefined, isNonEmptyString } from './type-checks'; const diagnosticCollection = vscode.languages.createDiagnosticCollection('calva'); @@ -68,7 +69,7 @@ function upsertTest( // Cider 0.26 and 0.27 have an issue where context can be an empty array. // https://github.com/clojure-emacs/cider-nrepl/issues/728#issuecomment-996002988 export function assertionName(result: cider.TestResult): string { - if (util.isNonEmptyString(result.context)) { + if (isNonEmptyString(result.context)) { return result.context; } return 'assertion'; @@ -181,9 +182,9 @@ function reportTests( diagnosticCollection.clear(); const recordDiagnostic = (result: cider.TestResult) => { - util.assertIsDefined(result.line, 'Expected cider test result to have a line!'); + assertIsDefined(result.line, 'Expected cider test result to have a line!'); - util.assertIsDefined(result.file, 'Expected cider test result to have a file!'); + assertIsDefined(result.file, 'Expected cider test result to have a file!'); const msg = cider.diagnosticMessage(result); diff --git a/src/type-checks.ts b/src/type-checks.ts new file mode 100644 index 000000000..8945a08e1 --- /dev/null +++ b/src/type-checks.ts @@ -0,0 +1,22 @@ +const isNonEmptyString = (value: any): boolean => typeof value == 'string' && value.length > 0; + +const isNullOrUndefined = (object: unknown): object is null | undefined => + object === null || object === undefined; + +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. +function assertIsDefined( + value: T | undefined | null, + message: string | (() => string) +): asserts value is T { + if (isNullOrUndefined(value)) { + console.trace({ value, message }); + throw new Error(typeof message === 'string' ? message : message()); + } +} + +export { isNonEmptyString, isNullOrUndefined, isDefined, assertIsDefined }; diff --git a/src/util/cursor-get-text.ts b/src/util/cursor-get-text.ts index 6b565ed55..e760416e7 100644 --- a/src/util/cursor-get-text.ts +++ b/src/util/cursor-get-text.ts @@ -3,6 +3,7 @@ */ import { EditableDocument } from '../cursor-doc/model'; +import { assertIsDefined } from '../type-checks'; export type RangeAndText = [[number, number], string] | [undefined, '']; @@ -11,7 +12,10 @@ export function currentTopLevelFunction( active: number = doc.selection.active ): RangeAndText { const defunCursor = doc.getTokenCursor(active); - const defunStart = defunCursor.rangeForDefun(active)[0]; + assertIsDefined(defunCursor, 'Expected a token cursor!'); + const defunRange = defunCursor.rangeForDefun(active); + assertIsDefined(defunRange, 'Expected a range representing the current defun!'); + const defunStart = defunRange[0]; const cursor = doc.getTokenCursor(defunStart); while (cursor.downList()) { cursor.forwardWhitespace(); @@ -41,7 +45,7 @@ export function currentTopLevelForm(doc: EditableDocument): RangeAndText { function rangeOrStartOfFileToCursor( doc: EditableDocument, - foldRange: [number, number], + foldRange: [number, number] | undefined, startFrom: number ): RangeAndText { if (foldRange) { @@ -65,12 +69,17 @@ function rangeOrStartOfFileToCursor( export function currentEnclosingFormToCursor(doc: EditableDocument): RangeAndText { const cursor = doc.getTokenCursor(doc.selection.active); const enclosingRange = cursor.rangeForList(1); + assertIsDefined(enclosingRange, 'Expected to get the range that encloses the current form!'); return rangeOrStartOfFileToCursor(doc, enclosingRange, enclosingRange[0]); } export function currentTopLevelFormToCursor(doc: EditableDocument): RangeAndText { const cursor = doc.getTokenCursor(doc.selection.active); const defunRange = cursor.rangeForDefun(doc.selection.active); + assertIsDefined( + defunRange, + 'Expected to get the range that encloses the current top-level form!' + ); return rangeOrStartOfFileToCursor(doc, defunRange, defunRange[0]); } diff --git a/src/utilities.ts b/src/utilities.ts index d79a8750c..54292801c 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -9,8 +9,6 @@ import * as JSZip from 'jszip'; 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 = '`'; @@ -23,29 +21,10 @@ 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, '\\$&'); } -export function isNonEmptyString(value: any): boolean { - return typeof value == 'string' && value.length > 0; -} - async function quickPickSingle(opts: { title?: string; values: string[]; @@ -88,7 +67,7 @@ async function quickPickMulti(opts: { values: string[]; saveAs: string; placeHol // Testing facility. // Recreated every time we create a new quickPick -let quickPickActive: Promise; +let quickPickActive: Promise | undefined; function quickPick( itemsToPick: string[], @@ -198,7 +177,7 @@ function tryToGetDocument( function getDocument(document: vscode.TextDocument | Record): vscode.TextDocument { const doc = tryToGetDocument(document); - if (isUndefined(doc)) { + if (doc === undefined) { throw new Error('Expected an activeTextEditor with a document!'); } @@ -530,7 +509,7 @@ function tryToGetActiveTextEditor(): vscode.TextEditor | undefined { function getActiveTextEditor(): vscode.TextEditor { const editor = tryToGetActiveTextEditor(); - if (isUndefined(editor)) { + if (editor === undefined) { throw new Error('Expected active text editor!'); } diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 3457663cc..90edf0907 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -6,7 +6,7 @@ import * as util from './utilities'; let lastContexts: context.CursorContext[] = []; -export function setCursorContextIfChanged(editor: vscode.TextEditor) { +export function setCursorContextIfChanged(editor: vscode.TextEditor | undefined) { if ( !editor || !editor.document || diff --git a/tsconfig.json b/tsconfig.json index a07a64cca..c47c8c1b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "incremental": true, "tsBuildInfoFile": "tsconfig.tsbuildinfo", "strict": false /* Enable all strict type-checking options. */, + "strictNullChecks": true /* Enable strict null checks. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */