diff --git a/html/help/script.ts b/html/help/script.ts index 014da4ba9..0e62dc287 100644 --- a/html/help/script.ts +++ b/html/help/script.ts @@ -1,9 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - declare function acquireVsCodeApi(): VsCode; const vscode = acquireVsCodeApi(); @@ -20,8 +14,10 @@ window.onmousedown = (ev) => { // handle requests from vscode ui -window.addEventListener('message', (ev) => { +window.addEventListener('message', (ev: MessageEvent) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const message = ev.data; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if(message.command === 'getScrollY'){ vscode.postMessage({ message: 'getScrollY', diff --git a/html/help/tsconfig.json b/html/help/tsconfig.json index 8ed7327c5..a3421d71d 100644 --- a/html/help/tsconfig.json +++ b/html/help/tsconfig.json @@ -8,7 +8,10 @@ "DOM" ], "sourceMap": false, - "strictNullChecks": true, - "rootDir": "." + "rootDir": ".", + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true } } diff --git a/html/httpgd/index.ts b/html/httpgd/index.ts index 7d2b416ad..401505733 100644 --- a/html/httpgd/index.ts +++ b/html/httpgd/index.ts @@ -30,7 +30,6 @@ const largePlotDiv = document.querySelector('#largePlot') as HTMLDivElement; const largeSvg = largePlotDiv.querySelector('svg') as SVGElement; const cssLink = document.querySelector('link.overwrites') as HTMLLinkElement; const smallPlotDiv = document.querySelector('#smallPlots') as HTMLDivElement; -const placeholderDiv = document.querySelector('#placeholder') as HTMLDivElement; function getSmallPlots(): HTMLAnchorElement[] { @@ -172,7 +171,7 @@ function togglePreviewPlotLayout(newStyle: PreviewPlotLayout): void { smallPlotDiv.classList.add(newStyle); } -function toggleFullWindowMode(useFullWindow): void { +function toggleFullWindowMode(useFullWindow: boolean): void { isFullWindow = useFullWindow; if(useFullWindow){ document.body.classList.add('fullWindow'); diff --git a/html/httpgd/tsconfig.json b/html/httpgd/tsconfig.json index 8ed7327c5..a3421d71d 100644 --- a/html/httpgd/tsconfig.json +++ b/html/httpgd/tsconfig.json @@ -8,7 +8,10 @@ "DOM" ], "sourceMap": false, - "strictNullChecks": true, - "rootDir": "." + "rootDir": ".", + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true } } diff --git a/package.json b/package.json index f3dd1cbf1..4c1692289 100644 --- a/package.json +++ b/package.json @@ -1915,6 +1915,7 @@ "@types/ejs": "^3.0.6", "@types/express": "^4.17.12", "@types/fs-extra": "^9.0.11", + "@types/glob": "^8.0.0", "@types/js-yaml": "^4.0.2", "@types/mocha": "^8.2.2", "@types/node": "^16.11.7", diff --git a/src/completions.ts b/src/completions.ts index 6a39d67a9..3fdbb0e79 100644 --- a/src/completions.ts +++ b/src/completions.ts @@ -30,7 +30,7 @@ const roxygenTagCompletionItems = [ export class HoverProvider implements vscode.HoverProvider { - provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover { + provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | null { if(!session.workspaceData?.globalenv){ return null; } @@ -56,7 +56,7 @@ export class HoverProvider implements vscode.HoverProvider { } export class HelpLinkHoverProvider implements vscode.HoverProvider { - async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise { + async provideHover(document: vscode.TextDocument, position: vscode.Position): Promise { if(!config().get('helpPanel.enableHoverLinks')){ return null; } @@ -114,7 +114,7 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider token: vscode.CancellationToken, completionContext: vscode.CompletionContext ): vscode.CompletionItem[] { - const items = []; + const items: vscode.CompletionItem[] = []; if (token.isCancellationRequested || !session.workspaceData?.globalenv) { return items; } @@ -148,7 +148,7 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider const symbol = document.getText(symbolRange); const doc = new vscode.MarkdownString('Element of `' + symbol + '`'); const obj = session.workspaceData.globalenv[symbol]; - let names: string[]; + let names: string[] | undefined; if (obj !== undefined) { if (completionContext.triggerCharacter === '$') { names = obj.names; @@ -187,14 +187,14 @@ function getCompletionItems(names: string[], kind: vscode.CompletionItemKind, de }); } -function getBracketCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) { +function getBracketCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.CompletionItem[] { const items: vscode.CompletionItem[] = []; - let range = new vscode.Range(new vscode.Position(position.line, 0), position); + let range: vscode.Range | undefined = new vscode.Range(new vscode.Position(position.line, 0), position); let expectOpenBrackets = 0; - let symbol: string; + let symbol: string | undefined = undefined; while (range) { - if (token.isCancellationRequested) { return; } + if (token.isCancellationRequested) { return []; } const text = document.getText(range); for (let i = text.length - 1; i >= 0; i -= 1) { const chr = text.charAt(i); @@ -212,7 +212,7 @@ function getBracketCompletionItems(document: vscode.TextDocument, position: vsco } } } - if (range?.start.line > 0) { + if (range?.start?.line !== undefined && range.start.line > 0) { range = document.lineAt(range.start.line - 1).range; // check previous line } else { range = undefined; @@ -229,10 +229,10 @@ function getBracketCompletionItems(document: vscode.TextDocument, position: vsco return items; } -function getPipelineCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) { +function getPipelineCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.CompletionItem[] { const items: vscode.CompletionItem[] = []; const range = extendSelection(position.line, (x) => document.lineAt(x).text, document.lineCount); - let symbol: string; + let symbol: string | undefined = undefined; for (let i = range.startLine; i <= range.endLine; i++) { if (token.isCancellationRequested) { diff --git a/src/cppProperties.ts b/src/cppProperties.ts index 8eba6fff4..ef477ef1b 100644 --- a/src/cppProperties.ts +++ b/src/cppProperties.ts @@ -1,10 +1,9 @@ 'use strict'; -import { randomBytes } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import { window } from 'vscode'; -import { getRpath, getCurrentWorkspaceFolder, executeRCommand } from './util'; +import { getRpath, getCurrentWorkspaceFolder, executeRCommand, createTempDir } from './util'; import { execSync } from 'child_process'; import { extensionContext } from './extension'; @@ -38,6 +37,9 @@ function platformChoose(win32: A, darwin: B, other: C): A | B | C { // See: https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference async function generateCppPropertiesProc(workspaceFolder: string) { const rPath = await getRpath(); + if (!rPath) { + return; + } // Collect information from running the compiler const configureFile = platformChoose('configure.win', 'configure', 'configure'); @@ -64,10 +66,10 @@ async function generateCppPropertiesProc(workspaceFolder: string) { const compileStdCpp = extractCompilerStd(compileOutputCpp); const compileStdC = extractCompilerStd(compileOutputC); const compileCall = extractCompilerCall(compileOutputCpp); - const compilerPath = await executeRCommand(`cat(Sys.which("${compileCall}"))`, workspaceFolder, (e: Error) => { + const compilerPath = compileCall ? await executeRCommand(`cat(Sys.which("${compileCall}"))`, workspaceFolder, (e: Error) => { void window.showErrorMessage(e.message); return ''; - }); + }) : ''; const intelliSensePlatform = platformChoose('windows', 'macos', 'linux'); const intelliSenseComp = compileCall ? (compileCall.includes('clang') ? 'clang' : 'gcc') : 'gcc'; @@ -175,13 +177,6 @@ function extractCompilerCall(compileOutput: string): string | undefined { return m?.[1]; } -function createTempDir(root: string): string { - let tempDir: string; - while (fs.existsSync(tempDir = path.join(root, `___temp_${randomBytes(8).toString('hex')}`))) { /* Name clash */ } - fs.mkdirSync(tempDir); - return tempDir; -} - function collectCompilerOutput(rPath: string, workspaceFolder: string, testExtension: 'cpp' | 'c') { const makevarsFiles = ['Makevars', 'Makevars.win', 'Makevars.ucrt']; diff --git a/src/extension.ts b/src/extension.ts index d46b69db4..d15bf329d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,7 +32,7 @@ export const tmpDir = (): string => util.getDir(path.join(homeExtDir(), 'tmp')); export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undefined; export let globalRHelp: rHelp.RHelp | undefined = undefined; export let extensionContext: vscode.ExtensionContext; -export let enableSessionWatcher: boolean = undefined; +export let enableSessionWatcher: boolean | undefined = undefined; export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined; export let rmdPreviewManager: rmarkdown.RMarkdownPreviewManager | undefined = undefined; export let rmdKnitManager: rmarkdown.RMarkdownKnitManager | undefined = undefined; @@ -80,10 +80,10 @@ export async function activate(context: vscode.ExtensionContext): Promise { void rTerminal.runSource(true); }, // rmd related - 'r.knitRmd': () => { void rmdKnitManager.knitRmd(false, undefined); }, - 'r.knitRmdToPdf': () => { void rmdKnitManager.knitRmd(false, 'pdf_document'); }, - 'r.knitRmdToHtml': () => { void rmdKnitManager.knitRmd(false, 'html_document'); }, - 'r.knitRmdToAll': () => { void rmdKnitManager.knitRmd(false, 'all'); }, + 'r.knitRmd': () => { void rmdKnitManager?.knitRmd(false, undefined); }, + 'r.knitRmdToPdf': () => { void rmdKnitManager?.knitRmd(false, 'pdf_document'); }, + 'r.knitRmdToHtml': () => { void rmdKnitManager?.knitRmd(false, 'html_document'); }, + 'r.knitRmdToAll': () => { void rmdKnitManager?.knitRmd(false, 'all'); }, 'r.selectCurrentChunk': rmarkdown.selectCurrentChunk, 'r.runCurrentChunk': rmarkdown.runCurrentChunk, 'r.runPreviousChunk': rmarkdown.runPreviousChunk, @@ -97,15 +97,15 @@ export async function activate(context: vscode.ExtensionContext): Promise rmarkdown.newDraft(), - 'r.rmarkdown.setKnitDirectory': () => rmdKnitManager.setKnitDir(), - 'r.rmarkdown.showPreviewToSide': () => rmdPreviewManager.previewRmd(vscode.ViewColumn.Beside), - 'r.rmarkdown.showPreview': (uri: vscode.Uri) => rmdPreviewManager.previewRmd(vscode.ViewColumn.Active, uri), - 'r.rmarkdown.preview.refresh': () => rmdPreviewManager.updatePreview(), - 'r.rmarkdown.preview.openExternal': () => void rmdPreviewManager.openExternalBrowser(), - 'r.rmarkdown.preview.showSource': () => rmdPreviewManager.showSource(), - 'r.rmarkdown.preview.toggleStyle': () => rmdPreviewManager.toggleTheme(), - 'r.rmarkdown.preview.enableAutoRefresh': () => rmdPreviewManager.enableAutoRefresh(), - 'r.rmarkdown.preview.disableAutoRefresh': () => rmdPreviewManager.disableAutoRefresh(), + 'r.rmarkdown.setKnitDirectory': () => rmdKnitManager?.setKnitDir(), + 'r.rmarkdown.showPreviewToSide': () => rmdPreviewManager?.previewRmd(vscode.ViewColumn.Beside), + 'r.rmarkdown.showPreview': (uri: vscode.Uri) => rmdPreviewManager?.previewRmd(vscode.ViewColumn.Active, uri), + 'r.rmarkdown.preview.refresh': () => rmdPreviewManager?.updatePreview(), + 'r.rmarkdown.preview.openExternal': () => void rmdPreviewManager?.openExternalBrowser(), + 'r.rmarkdown.preview.showSource': () => rmdPreviewManager?.showSource(), + 'r.rmarkdown.preview.toggleStyle': () => rmdPreviewManager?.toggleTheme(), + 'r.rmarkdown.preview.enableAutoRefresh': () => rmdPreviewManager?.enableAutoRefresh(), + 'r.rmarkdown.preview.disableAutoRefresh': () => rmdPreviewManager?.disableAutoRefresh(), // file creation (under file submenu) 'r.rmarkdown.newFileDraft': () => rmarkdown.newDraft(), @@ -133,8 +133,8 @@ export async function activate(context: vscode.ExtensionContext): Promise rWorkspace?.refresh(), - 'r.workspaceViewer.view': (node: workspaceViewer.GlobalEnvItem) => workspaceViewer.viewItem(node.label), - 'r.workspaceViewer.remove': (node: workspaceViewer.GlobalEnvItem) => workspaceViewer.removeItem(node.label), + 'r.workspaceViewer.view': (node: workspaceViewer.GlobalEnvItem) => node.label && workspaceViewer.viewItem(node.label), + 'r.workspaceViewer.remove': (node: workspaceViewer.GlobalEnvItem) => node.label && workspaceViewer.removeItem(node.label), 'r.workspaceViewer.clear': workspaceViewer.clearWorkspace, 'r.workspaceViewer.load': workspaceViewer.loadWorkspace, 'r.workspaceViewer.save': workspaceViewer.saveWorkspace, @@ -145,9 +145,8 @@ export async function activate(context: vscode.ExtensionContext): PromiseJSON.parse(json) || {}; } catch (e) { console.log(e); - void window.showErrorMessage((<{ message: string }>e).message); + void window.showErrorMessage(catchAsError(e).message); } } } diff --git a/src/helpViewer/index.ts b/src/helpViewer/index.ts index ea240bc49..c0e7f6c5c 100644 --- a/src/helpViewer/index.ts +++ b/src/helpViewer/index.ts @@ -28,6 +28,11 @@ export interface CodeClickConfig { 'Shift+Click': CodeClickAction, } const CODE_CLICKS: (keyof CodeClickConfig)[] = ['Click', 'Ctrl+Click', 'Shift+Click']; +export const codeClickConfigDefault = { + 'Click': 'Copy', + 'Ctrl+Click': 'Run', + 'Shift+Click': 'Ignore', +}; // Initialization function that is called once when activating the extension export async function initializeHelp( @@ -39,6 +44,9 @@ export async function initializeHelp( // get the "vanilla" R path from config const rPath = await getRpath(); + if(!rPath){ + return undefined; + } // get the current working directory from vscode const cwd = vscode.workspace.workspaceFolders?.length @@ -207,6 +215,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer { @@ -510,7 +519,7 @@ export class RHelp implements api.HelpPanel, vscode.WebviewPanelSerializer('helpPanel.clickCodeExamples'); - const isEnabled = CODE_CLICKS.some(k => codeClickConfig[k] !== 'Ignore'); + const isEnabled = CODE_CLICKS.some(k => codeClickConfig?.[k] !== 'Ignore'); if(isEnabled){ $('body').addClass('preClickable'); const codeSections = $('pre'); codeSections.each((i, section) => { const innerHtml = $(section).html(); + if(!innerHtml){ + return; + } const newPres = innerHtml.split('\n\n').map(s => s && `
${s}
`); const newHtml = '
' + newPres.join('\n') + '
'; $(section).replaceWith(newHtml); }); } - if(codeClickConfig.Click !== 'Ignore'){ + if(codeClickConfig?.Click !== 'Ignore'){ $('body').addClass('preHoverPointer'); } diff --git a/src/helpViewer/packages.ts b/src/helpViewer/packages.ts index 348763190..9661596a5 100644 --- a/src/helpViewer/packages.ts +++ b/src/helpViewer/packages.ts @@ -3,8 +3,7 @@ import * as cheerio from 'cheerio'; import * as vscode from 'vscode'; import { RHelp } from '.'; -import { getRpath, getConfirmation, executeAsTask, doWithProgress, getCranUrl } from '../util'; -import { AliasProvider } from './helpProvider'; +import { getConfirmation, executeAsTask, doWithProgress, getCranUrl } from '../util'; import { getPackagesFromCran } from './cran'; @@ -82,8 +81,6 @@ export class PackageManager { readonly rHelp: RHelp; - readonly aliasProvider: AliasProvider; - readonly state: vscode.Memento; readonly cwd?: string; @@ -195,7 +192,7 @@ export class PackageManager { // remove a specified package. The packagename is selected e.g. in the help tree-view public async removePackage(pkgName: string): Promise { - const rPath = await getRpath(); + const rPath = this.rHelp.rPath; const args = ['--silent', '--slave', '--no-save', '--no-restore', '-e', `remove.packages('${pkgName}')`]; const cmd = `${rPath} ${args.join(' ')}`; const confirmation = 'Yes, remove package!'; @@ -212,7 +209,7 @@ export class PackageManager { // actually install packages // confirmation can be skipped (e.g. if the user has confimred before) public async installPackages(pkgNames: string[], skipConfirmation: boolean = false): Promise { - const rPath = await getRpath(); + const rPath = this.rHelp.rPath; const cranUrl = await getCranUrl('', this.cwd); const args = [`--silent`, '--slave', `-e`, `install.packages(c(${pkgNames.map(v => `'${v}'`).join(',')}),repos='${cranUrl}')`]; const cmd = `${rPath} ${args.join(' ')}`; @@ -228,7 +225,7 @@ export class PackageManager { } public async updatePackages(skipConfirmation: boolean = false): Promise { - const rPath = await getRpath(); + const rPath = this.rHelp.rPath; const cranUrl = await getCranUrl('', this.cwd); const args = ['--silent', '--slave', '--no-save', '--no-restore', '-e', `update.packages(ask=FALSE,repos='${cranUrl}')`]; const cmd = `${rPath} ${args.join(' ')}`; diff --git a/src/helpViewer/panel.ts b/src/helpViewer/panel.ts index b13c28023..bcccdabec 100644 --- a/src/helpViewer/panel.ts +++ b/src/helpViewer/panel.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as cheerio from 'cheerio'; import { CodeClickConfig, HelpFile, RHelp } from '.'; -import { setContext, UriIcon, config } from '../util'; +import { setContext, UriIcon, config, asViewColumn } from '../util'; import { runTextInTerm } from '../rTerminal'; import { OutMessage } from './webviewMessages'; @@ -30,7 +30,6 @@ export class HelpPanel { // the webview panel where the help is shown public panel?: vscode.WebviewPanel; - private viewColumn: vscode.ViewColumn = vscode.ViewColumn.Two; // locations on disk, only changed on construction readonly webviewScriptFile: vscode.Uri; // the javascript added to help pages @@ -69,13 +68,19 @@ export class HelpPanel { for (const he of [...this.history, ...this.forwardHistory]) { he.isStale = true; } + if(!this.currentEntry){ + return; + } const newHelpFile = await this.rHelp.getHelpFileForPath(this.currentEntry.helpFile.requestPath); + if(!newHelpFile){ + return; + } newHelpFile.scrollY = await this.getScrollY(); await this.showHelpFile(newHelpFile, false, undefined, undefined, true); } // retrieves the stored webview or creates a new one if the webview was closed - private getWebview(preserveFocus: boolean = false): vscode.Webview { + private getWebview(preserveFocus: boolean = false, viewColumn: vscode.ViewColumn = vscode.ViewColumn.Two): vscode.Webview { // create webview if necessary if (!this.panel) { const webViewOptions: vscode.WebviewOptions & vscode.WebviewPanelOptions = { @@ -84,7 +89,7 @@ export class HelpPanel { retainContextWhenHidden: true // keep scroll position when not focussed }; const showOptions = { - viewColumn: this.viewColumn, + viewColumn: viewColumn, preserveFocus: preserveFocus }; this.panel = vscode.window.createWebviewPanel('rhelp', 'R Help', showOptions, webViewOptions); @@ -137,17 +142,12 @@ export class HelpPanel { // shows (internal) help file object in webview public async showHelpFile(helpFile: HelpFile | Promise, updateHistory = true, currentScrollY = 0, viewer?: vscode.ViewColumn | string, preserveFocus: boolean = false): Promise { - if (viewer === undefined) { - viewer = config().get('session.viewers.viewColumn.helpPanel'); - } - // update this.viewColumn if a valid viewer argument was supplied - if (typeof viewer === 'string') { - this.viewColumn = vscode.ViewColumn[String(viewer)]; - } + viewer ||= config().get('session.viewers.viewColumn.helpPanel'); + const viewColumn = asViewColumn(viewer); // get or create webview: - const webview = this.getWebview(preserveFocus); + const webview = this.getWebview(preserveFocus, viewColumn); // make sure helpFile is not a promise: helpFile = await helpFile; @@ -226,7 +226,9 @@ export class HelpPanel { private async showHistoryEntry(entry: HistoryEntry) { let helpFile: HelpFile; if (entry.isStale) { - helpFile = await this.rHelp.getHelpFileForPath(entry.helpFile.requestPath); + // Fallback to stale helpFile. + // Handle differently? + helpFile = await this.rHelp.getHelpFileForPath(entry.helpFile.requestPath) || entry.helpFile; helpFile.scrollY = entry.helpFile.scrollY; } else { helpFile = entry.helpFile; @@ -315,14 +317,14 @@ export class HelpPanel { // Check wheter to copy or run the code (or both or none) const codeClickConfig = config().get('helpPanel.clickCodeExamples'); const runCode = ( - isCtrlClick && codeClickConfig['Ctrl+Click'] === 'Run' - || isShiftClick && codeClickConfig['Shift+Click'] === 'Run' - || isNormalClick && codeClickConfig['Click'] === 'Run' + isCtrlClick && codeClickConfig?.['Ctrl+Click'] === 'Run' + || isShiftClick && codeClickConfig?.['Shift+Click'] === 'Run' + || isNormalClick && codeClickConfig?.['Click'] === 'Run' ); const copyCode = ( - isCtrlClick && codeClickConfig['Ctrl+Click'] === 'Copy' - || isShiftClick && codeClickConfig['Shift+Click'] === 'Copy' - || isNormalClick && codeClickConfig['Click'] === 'Copy' + isCtrlClick && codeClickConfig?.['Ctrl+Click'] === 'Copy' + || isShiftClick && codeClickConfig?.['Shift+Click'] === 'Copy' + || isNormalClick && codeClickConfig?.['Click'] === 'Copy' ); // Execute action: diff --git a/src/helpViewer/treeView.ts b/src/helpViewer/treeView.ts index f91cbbfe6..aede34752 100644 --- a/src/helpViewer/treeView.ts +++ b/src/helpViewer/treeView.ts @@ -30,7 +30,7 @@ const nodeCommands = { unsummarizeTopics: 'r.helpPanel.unsummarizeTopics', installPackages: 'r.helpPanel.installPackages', updateInstalledPackages: 'r.helpPanel.updateInstalledPackages' -}; +} as const; // used to avoid typos when handling commands type cmdName = keyof typeof nodeCommands; @@ -65,10 +65,11 @@ export class HelpTreeWrapper { // register the commands defiend in `nodeCommands` // they still need to be defined in package.json (apart from CALLBACK) for (const cmd in nodeCommands) { - extensionContext.subscriptions.push(vscode.commands.registerCommand(nodeCommands[cmd], (node: Node | undefined) => { - // treeview-root is represented by `undefined` + const cmdTyped = cmd as cmdName; // Ok since `cmdName` is defiend as `keyof typeof nodeCommands` + extensionContext.subscriptions.push(vscode.commands.registerCommand(nodeCommands[cmdTyped], (node: Node | undefined) => { + // treeview-root is represented by `undefined`: node ||= this.helpViewProvider.rootItem; - node.handleCommand(cmd); + node.handleCommand(cmdTyped); })); } } @@ -95,7 +96,7 @@ export class HelpViewProvider implements vscode.TreeDataProvider { this.rootItem = new RootNode(wrapper); } - onDidChangeTreeData(listener: (e: Node) => void): vscode.Disposable { + onDidChangeTreeData(listener: (e: Node | undefined) => void): vscode.Disposable { this.listeners.push(listener); return new vscode.Disposable(() => { // do nothing @@ -121,11 +122,11 @@ export class HelpViewProvider implements vscode.TreeDataProvider { // rather than modifying this class! abstract class Node extends vscode.TreeItem{ // TreeItem (defaults for this usecase) - public description: string; + public description?: string; public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None; public contextValue: string = ''; - public label: string; - public tooltip: string; + public label?: string; + public tooltip?: string; // set to null/undefined in derived class to expand/collapse on click public command = { @@ -135,7 +136,7 @@ abstract class Node extends vscode.TreeItem{ } as vscode.Command | undefined; // Node - public parent: Node | undefined; + public parent: Node | undefined = undefined; public children?: Node[] = undefined; // These can be used to modify the behaviour of a node when showed as/in a quickpick: @@ -146,25 +147,20 @@ abstract class Node extends vscode.TreeItem{ // These are shared between nodes to access functions of the help panel etc. // could also be static? - protected readonly wrapper: HelpTreeWrapper; - protected readonly rootNode: RootNode; - protected readonly rHelp: RHelp; + readonly wrapper: HelpTreeWrapper; + readonly rootNode?: RootNode; // used to give unique ids to nodes static newId: number = 0; // The default constructor just copies some info from parent - constructor(parent?: Node, wrapper?: HelpTreeWrapper){ + constructor(parent: Node | undefined, wrapper: HelpTreeWrapper){ super(''); + this.wrapper = wrapper; if(parent){ - wrapper ||= parent.wrapper; this.parent = parent; this.rootNode = parent.rootNode; } - if(wrapper){ - this.wrapper = wrapper; - this.rHelp = this.wrapper.rHelp; - } this.id = `${Node.newId++}`; } @@ -284,18 +280,19 @@ abstract class Node extends vscode.TreeItem{ } } -abstract class MetaNode extends Node { - // abstract parent class nodes that don't represent packages, topics etc. - // delete? +abstract class NonRootNode extends Node { + parent: RootNode | NonRootNode; + rootNode: RootNode; + public constructor(parent: RootNode | NonRootNode){ + super(parent, parent.wrapper); + this.parent = parent; + this.rootNode = parent.rootNode; + } } - - - - /////////////////////////////////// // The following classes contain the implementation of the help-view-specific behaviour // PkgRootNode, PackageNode, and TopicNode are a bit more complex @@ -304,11 +301,11 @@ abstract class MetaNode extends Node { // Root of the node. Is not actually used by vscode, but as 'imaginary' root item. -class RootNode extends MetaNode { +class RootNode extends Node { public collapsibleState = vscode.TreeItemCollapsibleState.Expanded; public label = 'root'; - public pkgRootNode: PkgRootNode; - protected readonly rootNode = this; + public pkgRootNode?: PkgRootNode; + readonly rootNode = this; constructor(wrapper: HelpTreeWrapper){ super(undefined, wrapper); @@ -331,7 +328,7 @@ class RootNode extends MetaNode { } // contains the list of installed packages -class PkgRootNode extends MetaNode { +class PkgRootNode extends NonRootNode { // TreeItem public label = 'Help Topics by Package'; public iconPath = new vscode.ThemeIcon('list-unordered'); @@ -342,7 +339,6 @@ class PkgRootNode extends MetaNode { // Node public children?: PackageNode[]; - public parent: RootNode; // quickpick public qpPrompt = 'Please select a package.'; @@ -395,14 +391,14 @@ class PkgRootNode extends MetaNode { refresh(clearCache: boolean = false, refreshChildren: boolean = true){ if(clearCache){ - this.rHelp.clearCachedFiles(`/doc/html/packages.html`); - void this.rHelp.packageManager.clearCachedFiles(`/doc/html/packages.html`); + this.wrapper.rHelp.clearCachedFiles(`/doc/html/packages.html`); + void this.wrapper.rHelp.packageManager.clearCachedFiles(`/doc/html/packages.html`); } super.refresh(refreshChildren); } async makeChildren() { - let packages = await this.rHelp.packageManager.getPackages(false); + let packages = await this.wrapper.rHelp.packageManager.getPackages(false); if(!packages){ return []; @@ -430,15 +426,12 @@ class PkgRootNode extends MetaNode { // contains the topics belonging to an individual package -export class PackageNode extends Node { +export class PackageNode extends NonRootNode { // TreeItem public command = undefined; public collapsibleState = CollapsibleState.Collapsed; public contextValue = Node.makeContextValue('QUICKPICK', 'clearCache', 'removePackage', 'updatePackage'); - // Node - public parent: PkgRootNode; - // QuickPick public qpPrompt = 'Please select a Topic.'; @@ -456,7 +449,7 @@ export class PackageNode extends Node { } else{ this.addContextValues('addToFavorites'); } - if(this.pkg.isFavorite && !this.parent.showOnlyFavorites){ + if(this.pkg.isFavorite && !this.rootNode.pkgRootNode?.showOnlyFavorites){ this.iconPath = new vscode.ThemeIcon('star-full'); } } @@ -464,23 +457,23 @@ export class PackageNode extends Node { public async _handleCommand(cmd: cmdName): Promise { if(cmd === 'clearCache'){ // useful e.g. when working on a package - this.rHelp.clearCachedFiles(new RegExp(`^/library/${this.pkg.name}/`)); + this.wrapper.rHelp.clearCachedFiles(new RegExp(`^/library/${this.pkg.name}/`)); this.refresh(); } else if(cmd === 'addToFavorites'){ - this.rHelp.packageManager.addFavorite(this.pkg.name); + this.wrapper.rHelp.packageManager.addFavorite(this.pkg.name); this.parent.refresh(); } else if(cmd === 'removeFromFavorites'){ - this.rHelp.packageManager.removeFavorite(this.pkg.name); + this.wrapper.rHelp.packageManager.removeFavorite(this.pkg.name); this.parent.refresh(); } else if(cmd === 'updatePackage'){ - const success = await this.rHelp.packageManager.installPackages([this.pkg.name]); + const success = await this.wrapper.rHelp.packageManager.installPackages([this.pkg.name]); // only reinstall if user confirmed removing the package (success === true) // might still refresh if install was attempted but failed if(success){ this.parent.refresh(true); } } else if(cmd === 'removePackage'){ - const success = await this.rHelp.packageManager.removePackage(this.pkg.name); + const success = await this.wrapper.rHelp.packageManager.removePackage(this.pkg.name); // only refresh if user confirmed removing the package (success === true) // might still refresh if removing was attempted but failed if(success){ @@ -491,23 +484,20 @@ export class PackageNode extends Node { async makeChildren(forQuickPick: boolean = false): Promise { const summarizeTopics = ( - forQuickPick ? false : (this.parent.summarizeTopics ?? true) + forQuickPick ? false : (this.rootNode.pkgRootNode?.summarizeTopics ?? true) ); - const topics = await this.rHelp.packageManager.getTopics(this.pkg.name, summarizeTopics, false); + const topics = await this.wrapper.rHelp.packageManager.getTopics(this.pkg.name, summarizeTopics, false); const ret = topics?.map(topic => new TopicNode(this, topic)) || []; return ret; } } // Node representing an individual topic/help page -class TopicNode extends Node { +class TopicNode extends NonRootNode { // TreeItem iconPath = new vscode.ThemeIcon('circle-filled'); contextValue = Node.makeContextValue('openInNewPanel'); - // Node - parent: PackageNode; - // Topic topic: Topic; @@ -520,10 +510,10 @@ class TopicNode extends Node { protected _handleCommand(cmd: cmdName){ if(cmd === 'CALLBACK'){ - void this.rHelp.showHelpForPath(this.topic.helpPath); + void this.wrapper.rHelp.showHelpForPath(this.topic.helpPath); } else if(cmd === 'openInNewPanel'){ - void this.rHelp.makeNewHelpPanel(); - void this.rHelp.showHelpForPath(this.topic.helpPath); + void this.wrapper.rHelp.makeNewHelpPanel(); + void this.wrapper.rHelp.showHelpForPath(this.topic.helpPath); } } @@ -547,7 +537,7 @@ class TopicNode extends Node { ///////////// // The following nodes only implement an individual command each -class HomeNode extends MetaNode { +class HomeNode extends NonRootNode { label = 'Home'; collapsibleState = CollapsibleState.None; iconPath = new vscode.ThemeIcon('home'); @@ -555,56 +545,54 @@ class HomeNode extends MetaNode { _handleCommand(cmd: cmdName){ if(cmd === 'openInNewPanel'){ - void this.rHelp.makeNewHelpPanel(); - void this.rHelp.showHelpForPath('doc/html/index.html'); + void this.wrapper.rHelp.makeNewHelpPanel(); + void this.wrapper.rHelp.showHelpForPath('doc/html/index.html'); } } callBack(){ - void this.rHelp.showHelpForPath('doc/html/index.html'); + void this.wrapper.rHelp.showHelpForPath('doc/html/index.html'); } } -class Search1Node extends MetaNode { +class Search1Node extends NonRootNode { label = 'Open Help Topic using `?`'; iconPath = new vscode.ThemeIcon('zap'); callBack(){ - void this.rHelp.searchHelpByAlias(); + void this.wrapper.rHelp.searchHelpByAlias(); } } -class Search2Node extends MetaNode { +class Search2Node extends NonRootNode { label = 'Search Help Topics using `??`'; iconPath = new vscode.ThemeIcon('search'); callBack(){ - void this.rHelp.searchHelpByText(); + void this.wrapper.rHelp.searchHelpByText(); } } -class RefreshNode extends MetaNode { - parent: RootNode; +class RefreshNode extends NonRootNode { label = 'Clear Cache & Restart Help Server'; iconPath = new vscode.ThemeIcon('refresh'); async callBack(){ - await doWithProgress(() => this.rHelp.refresh(), this.wrapper.viewId); - this.parent.pkgRootNode.refresh(); + await doWithProgress(() => this.wrapper.rHelp.refresh(), this.wrapper.viewId); + this.rootNode.pkgRootNode?.refresh(); } } -class OpenForSelectionNode extends MetaNode { - parent: RootNode; +class OpenForSelectionNode extends NonRootNode { label = 'Open Help Page for Selected Text'; iconPath = new vscode.ThemeIcon('symbol-key'); callBack(){ - void this.rHelp.openHelpForSelection(); + void this.wrapper.rHelp.openHelpForSelection(); } } -class InstallPackageNode extends MetaNode { +class InstallPackageNode extends NonRootNode { label = 'Install CRAN Package'; iconPath = new vscode.ThemeIcon('cloud-download'); @@ -612,21 +600,21 @@ class InstallPackageNode extends MetaNode { public async _handleCommand(cmd: cmdName){ if(cmd === 'installPackages'){ - const ret = await this.rHelp.packageManager.pickAndInstallPackages(true); + const ret = await this.wrapper.rHelp.packageManager.pickAndInstallPackages(true); if(ret){ - this.rootNode.pkgRootNode.refresh(true); + this.rootNode.pkgRootNode?.refresh(true); } } else if(cmd === 'updateInstalledPackages'){ - const ret = await this.rHelp.packageManager.updatePackages(); + const ret = await this.wrapper.rHelp.packageManager.updatePackages(); if(ret){ - this.rootNode.pkgRootNode.refresh(true); + this.rootNode.pkgRootNode?.refresh(true); } } } async callBack(){ - await this.rHelp.packageManager.pickAndInstallPackages(); - this.rootNode.pkgRootNode.refresh(true); + await this.wrapper.rHelp.packageManager.pickAndInstallPackages(); + this.rootNode.pkgRootNode?.refresh(true); } } diff --git a/src/languageService.ts b/src/languageService.ts index 0aced5366..17a69a0b2 100644 --- a/src/languageService.ts +++ b/src/languageService.ts @@ -1,13 +1,7 @@ -/* eslint-disable @typescript-eslint/await-thenable */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import os = require('os'); -import path = require('path'); -import net = require('net'); -import url = require('url'); +import * as os from 'os'; +import { dirname } from 'path'; +import * as net from 'net'; +import { URL } from 'url'; import { LanguageClient, LanguageClientOptions, StreamInfo, DocumentFilter, ErrorAction, CloseAction, RevealOutputChannelOn } from 'vscode-languageclient/node'; import { Disposable, workspace, Uri, TextDocument, WorkspaceConfiguration, OutputChannel, window, WorkspaceFolder } from 'vscode'; import { DisposableProcess, getRLibPaths, getRpath, promptToInstallRPackage, spawn } from './util'; @@ -26,15 +20,16 @@ export class LanguageService implements Disposable { return this.stopLanguageService(); } - private spawnServer(client: LanguageClient, rPath: string, args: readonly string[], options: CommonOptions): DisposableProcess { + private spawnServer(client: LanguageClient, rPath: string, args: readonly string[], options: CommonOptions & { cwd: string }): DisposableProcess { const childProcess = spawn(rPath, args, options); - client.outputChannel.appendLine(`R Language Server (${childProcess.pid}) started`); + const pid = childProcess.pid || -1; + client.outputChannel.appendLine(`R Language Server (${pid}) started`); childProcess.stderr.on('data', (chunk: Buffer) => { client.outputChannel.appendLine(chunk.toString()); }); childProcess.on('exit', (code, signal) => { - client.outputChannel.appendLine(`R Language Server (${childProcess.pid}) exited ` + - (signal ? `from signal ${signal}` : `with exit code ${code}`)); + client.outputChannel.appendLine(`R Language Server (${pid}) exited ` + + (signal ? `from signal ${signal}` : `with exit code ${code || 'null'}`)); if (code !== 0) { if (code === 10) { // languageserver is not installed. @@ -53,17 +48,17 @@ export class LanguageService implements Disposable { } private async createClient(config: WorkspaceConfiguration, selector: DocumentFilter[], - cwd: string, workspaceFolder: WorkspaceFolder, outputChannel: OutputChannel): Promise { + cwd: string, workspaceFolder: WorkspaceFolder | undefined, outputChannel: OutputChannel): Promise { let client: LanguageClient; const debug = config.get('lsp.debug'); - const rPath = await getRpath(); + const rPath = await getRpath() || ''; // TODO: Abort gracefully if (debug) { console.log(`R path: ${rPath}`); } const use_stdio = config.get('lsp.use_stdio'); - const env = Object.create(process.env); + const env = Object.create(process.env) as NodeJS.ProcessEnv; env.VSCR_LSP_DEBUG = debug ? 'TRUE' : 'FALSE'; env.VSCR_LIB_PATHS = getRLibPaths(); @@ -75,12 +70,13 @@ export class LanguageService implements Disposable { } if (debug) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.log(`LANG: ${env.LANG}`); } const rScriptPath = extensionContext.asAbsolutePath('R/languageServer.R'); const options = { cwd: cwd, env: env }; - const args = config.get('lsp.args').concat( + const args = (config.get('lsp.args') ?? []).concat( '--silent', '--slave', '--no-save', @@ -121,7 +117,7 @@ export class LanguageService implements Disposable { uriConverters: { // VS Code by default %-encodes even the colon after the drive letter // NodeJS handles it much better - code2Protocol: uri => new url.URL(uri.toString(true)).toString(), + code2Protocol: uri => new URL(uri.toString(true)).toString(), protocol2Code: str => Uri.parse(str) }, workspaceFolder: workspaceFolder, @@ -164,7 +160,7 @@ export class LanguageService implements Disposable { } this.initSet.add(name); const client = this.clients.get(name); - return client && client.needsStop(); + return (!!client) && client.needsStop(); } private getKey(uri: Uri): string { @@ -202,7 +198,7 @@ export class LanguageService implements Disposable { { scheme: 'vscode-notebook-cell', language: 'r', pattern: `${document.uri.fsPath}` }, ]; const client = await self.createClient(config, documentSelector, - path.dirname(document.uri.fsPath), folder, outputChannel); + dirname(document.uri.fsPath), folder, outputChannel); self.clients.set(key, client); self.initSet.delete(key); } @@ -252,7 +248,7 @@ export class LanguageService implements Disposable { { scheme: 'file', pattern: document.uri.fsPath }, ]; const client = await self.createClient(config, documentSelector, - path.dirname(document.uri.fsPath), undefined, outputChannel); + dirname(document.uri.fsPath), undefined, outputChannel); self.clients.set(key, client); self.initSet.delete(key); } diff --git a/src/lineCache.ts b/src/lineCache.ts index 9bf3d450b..fb3234eca 100644 --- a/src/lineCache.ts +++ b/src/lineCache.ts @@ -14,29 +14,27 @@ export class LineCache { this.lineCache = new Map(); this.endsInOperatorCache = new Map(); } - public addLineToCache(line: number): void { + // Returns [Line, EndsInOperator] + public addLineToCache(line: number): [string, boolean] { const cleaned = cleanLine(this.getLine(line)); const endsInOperator = doesLineEndInOperator(cleaned); this.lineCache.set(line, cleaned); this.endsInOperatorCache.set(line, endsInOperator); + return [cleaned, endsInOperator]; } public getEndsInOperatorFromCache(line: number): boolean { - const lineInCache = this.lineCache.has(line); - if (!lineInCache) { - this.addLineToCache(line); + const lineInCache = this.endsInOperatorCache.get(line); + if (lineInCache === undefined) { + return this.addLineToCache(line)[1]; } - const s = this.endsInOperatorCache.get(line); - - return (s); + return lineInCache; } public getLineFromCache(line: number): string { - const lineInCache = this.lineCache.has(line); - if (!lineInCache) { - this.addLineToCache(line); + const lineInCache = this.lineCache.get(line); + if (lineInCache === undefined) { + return this.addLineToCache(line)[0]; } - const s = this.lineCache.get(line); - - return (s); + return lineInCache; } } diff --git a/src/lintrConfig.ts b/src/lintrConfig.ts index 0f6431477..f48e6a4a2 100644 --- a/src/lintrConfig.ts +++ b/src/lintrConfig.ts @@ -5,7 +5,7 @@ import { join } from 'path'; import { window } from 'vscode'; import { executeRCommand, getCurrentWorkspaceFolder } from './util'; -export async function createLintrConfig(): Promise { +export async function createLintrConfig(): Promise { const currentWorkspaceFolder = getCurrentWorkspaceFolder()?.uri.fsPath; if (currentWorkspaceFolder === undefined) { void window.showWarningMessage('Please open a workspace folder to create .lintr'); diff --git a/src/liveShare/index.ts b/src/liveShare/index.ts index 74bf52a1b..e18305693 100644 --- a/src/liveShare/index.ts +++ b/src/liveShare/index.ts @@ -18,15 +18,15 @@ import { WorkspaceData, workspaceData } from '../session'; import { config } from '../util'; /// LiveShare -export let rHostService: HostService = undefined; -export let rGuestService: GuestService = undefined; +export let rHostService: HostService | undefined = undefined; +export let rGuestService: GuestService | undefined = undefined; export let liveSession: vsls.LiveShare; export let isGuestSession: boolean; export let _sessionStatusBarItem: vscode.StatusBarItem; // service vars export const ShareProviderName = 'vscode-r'; -export let service: vsls.SharedServiceProxy | vsls.SharedService | null = undefined; +export let service: vsls.SharedServiceProxy | vsls.SharedService | null = null; // random number to fake a UUID for differentiating between // host calls and guest calls (specifically for the workspace @@ -144,11 +144,11 @@ export async function LiveSessionListener(): Promise { break; case vsls.Role.Guest: console.log('[LiveSessionListener] guest event'); - await rGuestService.startService(); + await rGuestService?.startService(); break; case vsls.Role.Host: console.log('[LiveSessionListener] host event'); - await rHostService.startService(); + await rHostService?.startService(); rLiveShareProvider.refresh(); break; default: @@ -305,7 +305,7 @@ export class GuestService { // of having their own /tmp/ files public async requestFileContent(file: fs.PathLike | number): Promise; public async requestFileContent(file: fs.PathLike | number, encoding: string): Promise; - public async requestFileContent(file: fs.PathLike | number, encoding?: string): Promise { + public async requestFileContent(file: fs.PathLike | number, encoding?: string): Promise { if (this._isStarted) { if (encoding !== undefined) { const content: string | unknown = await liveShareRequest(Callback.GetFileContent, file, encoding); @@ -325,7 +325,7 @@ export class GuestService { } } - public async requestHelpContent(file: string): Promise { + public async requestHelpContent(file: string): Promise { const content: string | null | unknown = await liveShareRequest(Callback.GetHelpFileContent, file); if (content) { return content as HelpFile; @@ -341,7 +341,7 @@ export class GuestService { // This is used instead of relying on context disposables, // as an R session can continue even when liveshare is ended async function sessionCleanup(): Promise { - if (rHostService.isStarted()) { + if (rHostService?.isStarted()) { console.log('[HostService] stopping service'); await rHostService.stopService(); for (const [key, item] of browserDisposables.entries()) { diff --git a/src/liveShare/shareCommands.ts b/src/liveShare/shareCommands.ts index 1c46897d5..7745d2afd 100644 --- a/src/liveShare/shareCommands.ts +++ b/src/liveShare/shareCommands.ts @@ -62,7 +62,7 @@ export const Commands: ICommands = { // Command arguments are sent from the guest to the host, // and then the host sends the arguments to the console [Callback.RequestAttachGuest]: (): void => { - if (shareWorkspace) { + if (shareWorkspace && rHostService) { void rHostService.notifyRequest(requestFile, true); } else { void liveShareRequest(Callback.NotifyMessage, 'The host has not enabled guest attach.', MessageType.warning); @@ -76,8 +76,8 @@ export const Commands: ICommands = { } }, - [Callback.GetHelpFileContent]: (args: [text: string]): Promise => { - return globalRHelp.getHelpFileForPath(args[0]); + [Callback.GetHelpFileContent]: (args: [text: string]): Promise | undefined => { + return globalRHelp?.getHelpFileForPath(args[0]); }, /// File Handling /// // Host reads content from file, then passes the content diff --git a/src/liveShare/shareSession.ts b/src/liveShare/shareSession.ts index bc3cd44b3..b81d174c4 100644 --- a/src/liveShare/shareSession.ts +++ b/src/liveShare/shareSession.ts @@ -2,7 +2,7 @@ import path = require('path'); import * as vscode from 'vscode'; import { extensionContext, globalHttpgdManager, globalRHelp, rWorkspace } from '../extension'; -import { config, readContent } from '../util'; +import { asViewColumn, config, readContent } from '../util'; import { showBrowser, showDataView, showWebView, WorkspaceData } from '../session'; import { liveSession, UUID, rGuestService, _sessionStatusBarItem as sessionStatusBarItem } from '.'; import { autoShareBrowser } from './shareTree'; @@ -10,7 +10,7 @@ import { docProvider, docScheme } from './virtualDocs'; // Workspace Vars let guestPid: string; -export let guestWorkspace: WorkspaceData; +export let guestWorkspace: WorkspaceData | undefined; export let guestResDir: string; let rVer: string; let info: IRequest['info']; @@ -19,7 +19,7 @@ let info: IRequest['info']; // Used to keep track of shared browsers export const browserDisposables: { Disposable: vscode.Disposable, url: string, name: string }[] = []; -interface IRequest { +export interface IRequest { command: string; time?: string; pid?: string; @@ -57,7 +57,7 @@ export function initGuest(context: vscode.ExtensionContext): void { sessionStatusBarItem, vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider) ); - rGuestService.setStatusBarItem(sessionStatusBarItem); + rGuestService?.setStatusBarItem(sessionStatusBarItem); guestResDir = path.join(context.extensionPath, 'dist', 'resources'); } @@ -70,9 +70,9 @@ export function detachGuest(): void { } export function attachActiveGuest(): void { - if (config().get('sessionWatcher')) { + if (config().get('sessionWatcher', true)) { console.info('[attachActiveGuest]'); - void rGuestService.requestAttach(); + void rGuestService?.requestAttach(); } else { void vscode.window.showInformationMessage('This command requires that r.sessionWatcher be enabled.'); } @@ -82,7 +82,10 @@ export function attachActiveGuest(): void { // as this is handled by the session.ts variant // the force parameter is used for ensuring that the 'attach' case is appropriately called on guest join export async function updateGuestRequest(file: string, force: boolean = false): Promise { - const requestContent: string = await readContent(file, 'utf8'); + const requestContent: string | undefined = await readContent(file, 'utf8'); + if (!requestContent) { + return; + } console.info(`[updateGuestRequest] request: ${requestContent}`); if (typeof (requestContent) !== 'string') { return; @@ -107,7 +110,9 @@ export async function updateGuestRequest(file: string, force: boolean = false): case 'help': { if (globalRHelp) { console.log(request.requestPath); - await globalRHelp.showHelpForPath(request.requestPath, request.viewer); + if (request.requestPath) { + await globalRHelp.showHelpForPath(request.requestPath, request.viewer); + } } break; } @@ -123,21 +128,29 @@ export async function updateGuestRequest(file: string, force: boolean = false): info = request.info; console.info(`[updateGuestRequest] attach PID: ${guestPid}`); sessionStatusBarItem.text = `Guest R ${rVer}: ${guestPid}`; - sessionStatusBarItem.tooltip = `${info.version}\nProcess ID: ${guestPid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to host terminal.`; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + sessionStatusBarItem.tooltip = `${info?.version || 'unknown version'}\nProcess ID: ${guestPid}\nCommand: ${info?.command}\nStart time: ${info?.start_time}\nClick to attach to host terminal.`; sessionStatusBarItem.show(); break; } case 'browser': { - await showBrowser(request.url, request.title, request.viewer); + if (request.url && request.title && request.viewer) { + await showBrowser(request.url, request.title, request.viewer); + } break; } case 'webview': { - await showWebView(request.file, request.title, request.viewer); + if (request.file && request.title && request.viewer) { + await showWebView(request.file, request.title, request.viewer); + } break; } case 'dataview': { - await showDataView(request.source, - request.type, request.title, request.file, request.viewer); + if (request.source && request.type && request.title && request.file + && request.viewer) { + await showDataView(request.source, + request.type, request.title, request.file, request.viewer); + } break; } case 'rstudioapi': { @@ -162,11 +175,12 @@ export function updateGuestWorkspace(hostWorkspace: WorkspaceData): void { // Instead of creating a file, we pass the base64 of the plot image // to the guest, and read that into an html page -let panel: vscode.WebviewPanel = undefined; +let panel: vscode.WebviewPanel | undefined = undefined; export async function updateGuestPlot(file: string): Promise { const plotContent = await readContent(file, 'base64'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const guestPlotView: vscode.ViewColumn = vscode.ViewColumn[config().get('session.viewers.viewColumn.plot')]; + + const guestPlotView: vscode.ViewColumn = asViewColumn(config().get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two); if (plotContent) { if (panel) { panel.webview.html = getGuestImageHtml(plotContent); diff --git a/src/liveShare/shareTree.ts b/src/liveShare/shareTree.ts index d34024d83..eede8cc6a 100644 --- a/src/liveShare/shareTree.ts +++ b/src/liveShare/shareTree.ts @@ -11,9 +11,9 @@ export let rLiveShareProvider: LiveShareTreeProvider; export function initTreeView(): void { // get default bool values from settings - shareWorkspace = config().get('liveShare.defaults.shareWorkspace'); - forwardCommands = config().get('liveShare.defaults.commandForward'); - autoShareBrowser = config().get('liveShare.defaults.shareBrowser'); + shareWorkspace = config().get('liveShare.defaults.shareWorkspace', true); + forwardCommands = config().get('liveShare.defaults.commandForward', false); + autoShareBrowser = config().get('liveShare.defaults.shareBrowser', false); // create tree view for host controls rLiveShareProvider = new LiveShareTreeProvider(); @@ -37,7 +37,7 @@ export class LiveShareTreeProvider implements vscode.TreeDataProvider { // If a node needs to be collapsible, // change the element condition & return value - getChildren(element?: Node): Node[] { + getChildren(element?: Node): Node[] | undefined { if (element) { return; } else { @@ -48,8 +48,8 @@ export class LiveShareTreeProvider implements vscode.TreeDataProvider { // To add a tree item to the LiveShare R view, // write a class object that extends Node and // add it to the list of nodes here - private getNodes(): Node[] { - let items: Node[] = undefined; + private getNodes(): Node[] | undefined { + let items: Node[] | undefined = undefined; if (isLiveShare()) { items = [ new ShareNode(), @@ -64,12 +64,12 @@ export class LiveShareTreeProvider implements vscode.TreeDataProvider { // Base class for adding to abstract class Node extends vscode.TreeItem { - public label: string; - public tooltip: string; - public contextValue: string; - public description: string; - public iconPath: vscode.ThemeIcon; - public collapsibleState: vscode.TreeItemCollapsibleState; + public label?: string; + public tooltip?: string; + public contextValue?: string; + public description?: string; + public iconPath?: vscode.ThemeIcon; + public collapsibleState?: vscode.TreeItemCollapsibleState; constructor() { super(''); @@ -82,12 +82,12 @@ abstract class Node extends vscode.TreeItem { // If a toggle is not required, extend a different Node type. export abstract class ToggleNode extends Node { public toggle(treeProvider: LiveShareTreeProvider): void { treeProvider.refresh(); } - public label: string; - public tooltip: string; - public contextValue: string; - public description: string; - public iconPath: vscode.ThemeIcon; - public collapsibleState: vscode.TreeItemCollapsibleState; + public label?: string; + public tooltip?: string; + public contextValue?: string; + public description?: string; + public iconPath?: vscode.ThemeIcon; + public collapsibleState?: vscode.TreeItemCollapsibleState; constructor(bool: boolean) { super(); @@ -102,9 +102,9 @@ class ShareNode extends ToggleNode { shareWorkspace = !shareWorkspace; this.description = shareWorkspace === true ? 'Enabled' : 'Disabled'; if (shareWorkspace) { - void rHostService.notifyRequest(requestFile, true); + void rHostService?.notifyRequest(requestFile, true); } else { - void rHostService.orderGuestDetach(); + void rHostService?.orderGuestDetach(); } treeProvider.refresh(); } @@ -112,7 +112,7 @@ class ShareNode extends ToggleNode { public label: string = 'Share R Workspace'; public tooltip: string = 'Whether guests can access the current R session and its workspace'; public contextValue: string = 'shareNode'; - public description: string; + public description?: string; public iconPath: vscode.ThemeIcon = new vscode.ThemeIcon('broadcast'); public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None; diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 519d985c5..252ad762e 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as ejs from 'ejs'; -import { config, setContext, UriIcon } from '../util'; +import { asViewColumn, config, setContext, UriIcon } from '../util'; import { extensionContext } from '../extension'; @@ -286,12 +286,12 @@ export class HttpgdViewer implements IHttpgdViewer { customOverwriteCssPath?: string; // Size of the view area: - viewHeight: number; - viewWidth: number; + viewHeight: number = 600; + viewWidth: number = 800; // Size of the shown plot (as computed): - plotHeight: number; - plotWidth: number; + plotHeight: number = 600; + plotWidth: number = 800; readonly zoom0: number = 1; zoom: number = this.zoom0; @@ -358,7 +358,7 @@ export class HttpgdViewer implements IHttpgdViewer { this.htmlTemplate = fs.readFileSync(path.join(this.htmlRoot, 'index.ejs'), 'utf-8'); this.smallPlotTemplate = fs.readFileSync(path.join(this.htmlRoot, 'smallPlot.ejs'), 'utf-8'); this.showOptions = { - viewColumn: options.viewColumn ?? vscode.ViewColumn[conf.get('session.viewers.viewColumn.plot') || 'Two'], + viewColumn: options.viewColumn ?? asViewColumn(conf.get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two), preserveFocus: !!options.preserveFocus }; this.webviewOptions = { @@ -843,7 +843,7 @@ export class HttpgdViewer implements IHttpgdViewer { `Export failed: ${err.message}` )); dest.on('close', () => void vscode.window.showInformationMessage( - `Export done: ${outFile}` + `Export done: ${outFile || ''}` )); void plt.body.pipe(dest); } diff --git a/src/preview.ts b/src/preview.ts index 35b12fe96..6df57035d 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,11 +1,11 @@ 'use strict'; -import { existsSync, mkdirSync, removeSync, statSync } from 'fs-extra'; +import { removeSync, statSync } from 'fs-extra'; import { commands, extensions, window, workspace } from 'vscode'; import { runTextInTerm } from './rTerminal'; import { getWordOrSelection } from './selection'; -import { config, checkForSpecialCharacters, checkIfFileExists, delay } from './util'; +import { config, checkForSpecialCharacters, checkIfFileExists, delay, createTempDir, getCurrentWorkspaceFolder } from './util'; export async function previewEnvironment(): Promise { if (config().get('sessionWatcher')) { @@ -14,7 +14,11 @@ export async function previewEnvironment(): Promise { if (!checkcsv()) { return; } - const tmpDir = makeTmpDir(); + const currentWorkspaceFolder = getCurrentWorkspaceFolder()?.uri.fsPath; + if (!currentWorkspaceFolder) { + return; + } + const tmpDir = createTempDir(currentWorkspaceFolder, true); const pathToTmpCsv = `${tmpDir}/environment.csv`; const envName = 'name=ls()'; const envClass = 'class=sapply(ls(), function(x) {class(get(x, envir = parent.env(environment())))[1]})'; @@ -29,16 +33,23 @@ export async function previewEnvironment(): Promise { } } -export async function previewDataframe(): Promise { +export async function previewDataframe(): Promise { if (config().get('sessionWatcher')) { const symbol = getWordOrSelection(); - await runTextInTerm(`View(${symbol})`); + await runTextInTerm(`View(${symbol || 'undefined'})`); } else { if (!checkcsv()) { - return undefined; + return; + } + const currentWorkspaceFolder = getCurrentWorkspaceFolder()?.uri.fsPath; + if (!currentWorkspaceFolder) { + return; } const dataframeName = getWordOrSelection(); + if (!dataframeName) { + return; + } if (!checkForSpecialCharacters(dataframeName)) { void window.showInformationMessage('This does not appear to be a dataframe.'); @@ -46,7 +57,7 @@ export async function previewDataframe(): Promise { return false; } - const tmpDir = makeTmpDir(); + const tmpDir = createTempDir(currentWorkspaceFolder, true); // Create R write CSV command. Turn off row names and quotes, they mess with Excel Viewer. const pathToTmpCsv = `${tmpDir}/${dataframeName}.csv`; @@ -57,7 +68,7 @@ export async function previewDataframe(): Promise { } } -async function openTmpCSV(pathToTmpCsv: string, tmpDir: string) { +async function openTmpCSV(pathToTmpCsv: string, tmpDir: string): Promise { await delay(350); // Needed since file size has not yet changed if (!checkIfFileExists(pathToTmpCsv)) { @@ -85,7 +96,7 @@ async function openTmpCSV(pathToTmpCsv: string, tmpDir: string) { ); } -async function waitForFileToFinish(filePath: string) { +async function waitForFileToFinish(filePath: string): Promise { const fileBusy = true; let currentSize = 0; let previousSize = 1; @@ -108,22 +119,7 @@ async function waitForFileToFinish(filePath: string) { } } -function makeTmpDir() { - let tmpDir = workspace.workspaceFolders[0].uri.fsPath; - if (process.platform === 'win32') { - tmpDir = tmpDir.replace(/\\/g, '/'); - tmpDir += '/tmp'; - } else { - tmpDir += '/.tmp'; - } - if (!existsSync(tmpDir)) { - mkdirSync(tmpDir); - } - - return tmpDir; -} - -function checkcsv() { +function checkcsv(): boolean { const iscsv = extensions.getExtension('GrapeCity.gc-excelviewer'); if (iscsv !== undefined && iscsv.isActive) { return true; diff --git a/src/rGitignore.ts b/src/rGitignore.ts index b508121f0..93e72dfd7 100644 --- a/src/rGitignore.ts +++ b/src/rGitignore.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import { window } from 'vscode'; import { extensionContext } from './extension'; -import { getCurrentWorkspaceFolder } from './util'; +import { catchAsError, getCurrentWorkspaceFolder } from './util'; export async function createGitignore(): Promise { // .gitignore template from "https://github.com/github/gitignore/blob/main/R.gitignore" @@ -34,7 +34,7 @@ export async function createGitignore(): Promise { void window.showErrorMessage(err.name); } } catch (e) { - void window.showErrorMessage(e); + void window.showErrorMessage(catchAsError(e).message); } }); } diff --git a/src/rTerminal.ts b/src/rTerminal.ts index 52def362d..38eeb71d9 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -13,16 +13,22 @@ import { removeSessionFiles } from './session'; import { config, delay, getRterm } from './util'; import { rGuestService, isGuestSession } from './liveShare'; import * as fs from 'fs'; -export let rTerm: vscode.Terminal; +export let rTerm: vscode.Terminal | undefined = undefined; export async function runSource(echo: boolean): Promise { const wad = vscode.window.activeTextEditor?.document; + if (!wad) { + return; + } const isSaved = await util.saveDocument(wad); if (!isSaved) { return; } let rPath: string = util.ToRStringLiteral(wad.fileName, '"'); let encodingParam = util.config().get('source.encoding'); + if (encodingParam === undefined) { + return; + } encodingParam = `encoding = "${encodingParam}"`; rPath = [rPath, encodingParam].join(', '); if (echo) { @@ -41,18 +47,28 @@ export async function runSelectionRetainCursor(): Promise { export async function runSelectionOrWord(rFunctionName: string[]): Promise { const text = selection.getWordOrSelection(); + if (!text) { + return; + } const wrappedText = selection.surroundSelection(text, rFunctionName); await runTextInTerm(wrappedText); } export async function runCommandWithSelectionOrWord(rCommand: string): Promise { const text = selection.getWordOrSelection(); + if (!text) { + return; + } const call = rCommand.replace(/\$\$/g, text); await runTextInTerm(call); } export async function runCommandWithEditorPath(rCommand: string): Promise { - const wad: vscode.TextDocument = vscode.window.activeTextEditor.document; + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return; + } + const wad: vscode.TextDocument = textEditor.document; const isSaved = await util.saveDocument(wad); if (isSaved) { const rPath = util.ToRStringLiteral(wad.fileName, ''); @@ -66,26 +82,37 @@ export async function runCommand(rCommand: string): Promise { } export async function runFromBeginningToLine(): Promise { - const endLine = vscode.window.activeTextEditor.selection.end.line; - const charactersOnLine = vscode.window.activeTextEditor.document.lineAt(endLine).text.length; + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return; + } + const endLine = textEditor.selection.end.line; + const charactersOnLine = textEditor.document.lineAt(endLine).text.length; const endPos = new vscode.Position(endLine, charactersOnLine); const range = new vscode.Range(new vscode.Position(0, 0), endPos); - const text = vscode.window.activeTextEditor.document.getText(range); + const text = textEditor.document.getText(range); + if (text === undefined) { + return; + } await runTextInTerm(text); } export async function runFromLineToEnd(): Promise { - const startLine = vscode.window.activeTextEditor.selection.start.line; + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return; + } + const startLine = textEditor.selection.start.line; const startPos = new vscode.Position(startLine, 0); - const endLine = vscode.window.activeTextEditor.document.lineCount; + const endLine = textEditor.document.lineCount; const range = new vscode.Range(startPos, new vscode.Position(endLine, 0)); - const text = vscode.window.activeTextEditor.document.getText(range); + const text = textEditor.document.getText(range); await runTextInTerm(text); } export async function makeTerminalOptions(): Promise { const termPath = await getRterm(); - const shellArgs: string[] = config().get('rterm.option'); + const shellArgs: string[] = config().get('rterm.option') || []; const termOptions: vscode.TerminalOptions = { name: 'R Interactive', shellPath: termPath, @@ -136,7 +163,7 @@ export function deleteTerminal(term: vscode.Terminal): void { } } -export async function chooseTerminal(): Promise { +export async function chooseTerminal(): Promise { if (config().get('alwaysUseActiveTerminal')) { if (vscode.window.terminals.length < 1) { void vscode.window.showInformationMessage('There are no open terminals.'); @@ -201,11 +228,18 @@ export async function chooseTerminal(): Promise { export async function runSelectionInTerm(moveCursor: boolean, useRepl = true): Promise { const selection = getSelection(); + if (!selection) { + return; + } if (moveCursor && selection.linesDownToMoveCursor > 0) { - const lineCount = vscode.window.activeTextEditor.document.lineCount; - if (selection.linesDownToMoveCursor + vscode.window.activeTextEditor.selection.end.line === lineCount) { - const endPos = new vscode.Position(lineCount, vscode.window.activeTextEditor.document.lineAt(lineCount - 1).text.length); - await vscode.window.activeTextEditor.edit(e => e.insert(endPos, '\n')); + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return; + } + const lineCount = textEditor.document.lineCount; + if (selection.linesDownToMoveCursor + textEditor.selection.end.line === lineCount) { + const endPos = new vscode.Position(lineCount, textEditor.document.lineAt(lineCount - 1).text.length); + await textEditor.edit(e => e.insert(endPos, '\n')); } await vscode.commands.executeCommand('cursorMove', { to: 'down', value: selection.linesDownToMoveCursor }); await vscode.commands.executeCommand('cursorMove', { to: 'wrappedLineFirstNonWhitespaceCharacter' }); @@ -218,8 +252,12 @@ export async function runSelectionInTerm(moveCursor: boolean, useRepl = true): P } export async function runChunksInTerm(chunks: vscode.Range[]): Promise { + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return; + } const text = chunks - .map((chunk) => vscode.window.activeTextEditor.document.getText(chunk).trim()) + .map((chunk) => textEditor.document.getText(chunk).trim()) .filter((chunk) => chunk.length > 0) .join('\n'); if (text.length > 0) { @@ -229,7 +267,7 @@ export async function runChunksInTerm(chunks: vscode.Range[]): Promise { export async function runTextInTerm(text: string, execute: boolean = true): Promise { if (isGuestSession) { - rGuestService.requestRunTextInTerm(text); + rGuestService?.requestRunTextInTerm(text); } else { const term = await chooseTerminal(); if (term === undefined) { @@ -242,7 +280,7 @@ export async function runTextInTerm(text: string, execute: boolean = true): Prom } term.sendText(text, execute); } else { - const rtermSendDelay: number = config().get('rtermSendDelay'); + const rtermSendDelay: number = config().get('rtermSendDelay') || 8; const split = text.split('\n'); const last_split = split.length - 1; for (const [count, line] of split.entries()) { @@ -265,7 +303,7 @@ export async function runTextInTerm(text: string, execute: boolean = true): Prom } function setFocus(term: vscode.Terminal) { - const focus: string = config().get('source.focus'); + const focus: string = config().get('source.focus') || 'editor'; if (focus !== 'none') { term.show(focus !== 'terminal'); } @@ -273,6 +311,9 @@ function setFocus(term: vscode.Terminal) { export async function sendRangeToRepl(rng: vscode.Range): Promise { const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } const sel0 = editor.selections; let sel1 = new vscode.Selection(rng.start, rng.end); while(/^[\r\n]/.exec(editor.document.getText(sel1))){ diff --git a/src/rmarkdown/draft.ts b/src/rmarkdown/draft.ts index 0617993f9..172eb1b68 100644 --- a/src/rmarkdown/draft.ts +++ b/src/rmarkdown/draft.ts @@ -1,6 +1,6 @@ import { QuickPickItem, QuickPickOptions, Uri, window, workspace, env } from 'vscode'; import { extensionContext } from '../extension'; -import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync, getConfirmation } from '../util'; +import { executeRCommand, getCurrentWorkspaceFolder, getRpath, ToRStringLiteral, spawnAsync, getConfirmation, catchAsError } from '../util'; import * as cp from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -18,9 +18,12 @@ interface TemplateItem extends QuickPickItem { info: TemplateInfo; } -async function getTemplateItems(cwd: string): Promise { +async function getTemplateItems(cwd: string): Promise { const lim = '---vsc---'; const rPath = await getRpath(); + if (!rPath) { + return undefined; + } const options: cp.CommonOptions = { cwd: cwd, env: { @@ -46,7 +49,7 @@ async function getTemplateItems(cwd: string): Promise { } const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); const match = re.exec(result.stdout); - if (match.length !== 2) { + if (!match || match.length !== 2) { throw new Error('Could not parse R output.'); } const json = match[1]; @@ -64,12 +67,12 @@ async function getTemplateItems(cwd: string): Promise { return items; } catch (e) { console.log(e); - void window.showErrorMessage((<{ message: string }>e).message); + void window.showErrorMessage(catchAsError(e).message); return undefined; } } -async function launchTemplatePicker(cwd: string): Promise { +async function launchTemplatePicker(cwd: string): Promise { const options: QuickPickOptions = { matchOnDescription: true, matchOnDetail: true, @@ -87,7 +90,7 @@ async function launchTemplatePicker(cwd: string): Promise { return selection; } else { void window.showInformationMessage('No templates found. Would you like to browse the wiki page for R packages that provide R Markdown templates?', 'Yes', 'No') - .then((select: string) => { + .then((select: string | undefined) => { if (select === 'Yes') { void env.openExternal(Uri.parse('https://github.com/REditorSupport/vscode-R/wiki/R-Markdown#templates')); } @@ -97,7 +100,7 @@ async function launchTemplatePicker(cwd: string): Promise { return undefined; } -async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { +async function makeDraft(file: string, template: TemplateItem, cwd: string): Promise { const fileString = ToRStringLiteral(file, ''); const cmd = `cat(normalizePath(rmarkdown::draft(file='${fileString}', template='${template.info.id}', package='${template.info.package}', edit=FALSE)))`; return await executeRCommand(cmd, cwd, (e: Error) => { diff --git a/src/rmarkdown/index.ts b/src/rmarkdown/index.ts index ef448bb00..65bc4a71f 100644 --- a/src/rmarkdown/index.ts +++ b/src/rmarkdown/index.ts @@ -60,17 +60,20 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { this.codeLenses = []; const chunks = getChunks(document); const chunkRanges: vscode.Range[] = []; - const rmdCodeLensCommands: string[] = config().get('rmarkdown.codeLensCommands'); + const rmdCodeLensCommands: string[] = config().get('rmarkdown.codeLensCommands', []); // Iterate through all code chunks for getting chunk information for both CodeLens and chunk background color (set by `editor.setDecorations`) for (let i = 1; i <= chunks.length; i++) { const chunk = chunks.find(e => e.id === i); + if (!chunk) { + continue; + } const chunkRange = chunk.chunkRange; const line = chunk.startLine; chunkRanges.push(chunkRange); // Enable/disable only CodeLens, without affecting chunk background color. - if (config().get('rmarkdown.enableCodeLens') && (chunk.language === 'r') || isRDocument(document)) { + if (config().get('rmarkdown.enableCodeLens', true) && (chunk.language === 'r') || isRDocument(document)) { if (token.isCancellationRequested) { break; } @@ -148,8 +151,9 @@ export class RMarkdownCodeLensProvider implements vscode.CodeLensProvider { // For default options, both options and sort order are based on options specified in package.json. // For user-specified options, both options and sort order are based on options specified in settings UI or settings.json. return this.codeLenses. - filter(e => rmdCodeLensCommands.includes(e.command.command)). + filter(e => e.command && rmdCodeLensCommands.includes(e.command.command)). sort(function (a, b) { + if (!a.command || !b.command) { return 0; } const sorted = rmdCodeLensCommands.indexOf(a.command.command) - rmdCodeLensCommands.indexOf(b.command.command); return sorted; @@ -164,9 +168,9 @@ interface RMarkdownChunk { id: number; startLine: number; endLine: number; - language: string; - options: string; - eval: boolean; + language: string | undefined; + options: string | undefined; + eval: boolean | undefined; chunkRange: vscode.Range; codeRange: vscode.Range; } @@ -178,11 +182,11 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] { let line = 0; let chunkId = 0; // One-based index - let chunkStartLine: number = undefined; - let chunkEndLine: number = undefined; - let chunkLanguage: string = undefined; - let chunkOptions: string = undefined; - let chunkEval: boolean = undefined; + let chunkStartLine: number | undefined = undefined; + let chunkEndLine: number | undefined = undefined; + let chunkLanguage: string | undefined = undefined; + let chunkOptions: string | undefined = undefined; + let chunkEval: boolean | undefined = undefined; const isRDoc = isRDocument(document); while (line < lines.length) { @@ -226,14 +230,20 @@ export function getChunks(document: vscode.TextDocument): RMarkdownChunk[] { return chunks; } -function getCurrentChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { - const lines = vscode.window.activeTextEditor.document.getText().split(/\r?\n/); +function getCurrentChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk | undefined { + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + void vscode.window.showWarningMessage('No text editor active.'); + return; + } + + const lines = textEditor.document.getText().split(/\r?\n/); let chunkStartLineAtOrAbove = line; // `- 1` to cover edge case when cursor is at 'chunk end line' let chunkEndLineAbove = line - 1; - const isRDoc = isRDocument(vscode.window.activeTextEditor.document); + const isRDoc = isRDocument(textEditor.document); while (chunkStartLineAtOrAbove >= 0 && !isChunkStartLine(lines[chunkStartLineAtOrAbove], isRDoc)) { chunkStartLineAtOrAbove--; @@ -261,12 +271,15 @@ function getCurrentChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk // Alternative `getCurrentChunk` for cases: // - commands (e.g. `selectCurrentChunk`) only make sense when cursor is within chunk // - when cursor is outside of chunk, no response is triggered for chunk navigation commands (e.g. `goToPreviousChunk`) and chunk running commands (e.g. `runAboveChunks`) -function getCurrentChunk__CursorWithinChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { +function getCurrentChunk__CursorWithinChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk | undefined { return chunks.find(i => i.startLine <= line && i.endLine >= line); } -function getPreviousChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { +function getPreviousChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk | undefined { const currentChunk = getCurrentChunk(chunks, line); + if (!currentChunk) { + return undefined; + } if (currentChunk.id !== 1) { // When cursor is below the last 'chunk end line', the definition of the previous chunk is the last chunk const previousChunkId = currentChunk.endLine < line ? currentChunk.id : currentChunk.id - 1; @@ -277,8 +290,11 @@ function getPreviousChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChun } } -function getNextChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { +function getNextChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk | undefined { const currentChunk = getCurrentChunk(chunks, line); + if (!currentChunk) { + return undefined; + } if (currentChunk.id !== chunks.length) { // When cursor is above the first 'chunk start line', the definition of the next chunk is the first chunk const nextChunkId = line < currentChunk.startLine ? currentChunk.id : currentChunk.id + 1; @@ -291,17 +307,27 @@ function getNextChunk(chunks: RMarkdownChunk[], line: number): RMarkdownChunk { } // Helpers -function _getChunks() { - return getChunks(vscode.window.activeTextEditor.document); +function _getChunks(): RMarkdownChunk[] { + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return []; + } + return getChunks(textEditor.document); } -function _getStartLine() { - return vscode.window.activeTextEditor.selection.start.line; +function _getStartLine(): number { + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + return 0; + } + return textEditor.selection.start.line; } export async function runCurrentChunk(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): Promise { const currentChunk = getCurrentChunk(chunks, line); - await runChunksInTerm([currentChunk.codeRange]); + if (currentChunk) { + await runChunksInTerm([currentChunk.codeRange]); + } } export async function runPreviousChunk(chunks: RMarkdownChunk[] = _getChunks(), @@ -309,7 +335,7 @@ export async function runPreviousChunk(chunks: RMarkdownChunk[] = _getChunks(), const currentChunk = getCurrentChunk(chunks, line); const previousChunk = getPreviousChunk(chunks, line); - if (previousChunk !== currentChunk) { + if (previousChunk && previousChunk !== currentChunk) { await runChunksInTerm([previousChunk.codeRange]); } @@ -320,7 +346,7 @@ export async function runNextChunk(chunks: RMarkdownChunk[] = _getChunks(), const currentChunk = getCurrentChunk(chunks, line); const nextChunk = getNextChunk(chunks, line); - if (nextChunk !== currentChunk) { + if (nextChunk && nextChunk !== currentChunk) { await runChunksInTerm([nextChunk.codeRange]); } } @@ -329,6 +355,9 @@ export async function runAboveChunks(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): Promise { const currentChunk = getCurrentChunk(chunks, line); const previousChunk = getPreviousChunk(chunks, line); + if (!currentChunk || !previousChunk) { + return; + } const firstChunkId = 1; const previousChunkId = previousChunk.id; @@ -337,7 +366,7 @@ export async function runAboveChunks(chunks: RMarkdownChunk[] = _getChunks(), if (previousChunk !== currentChunk) { for (let i = firstChunkId; i <= previousChunkId; i++) { const chunk = chunks.find(e => e.id === i); - if (chunk.eval) { + if (chunk?.eval) { codeRanges.push(chunk.codeRange); } } @@ -350,6 +379,9 @@ export async function runBelowChunks(chunks: RMarkdownChunk[] = _getChunks(), const currentChunk = getCurrentChunk(chunks, line); const nextChunk = getNextChunk(chunks, line); + if (!currentChunk || !nextChunk) { + return; + } const nextChunkId = nextChunk.id; const lastChunkId = chunks.length; @@ -357,7 +389,7 @@ export async function runBelowChunks(chunks: RMarkdownChunk[] = _getChunks(), if (nextChunk !== currentChunk) { for (let i = nextChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); - if (chunk.eval) { + if (chunk?.eval) { codeRanges.push(chunk.codeRange); } } @@ -368,6 +400,9 @@ export async function runBelowChunks(chunks: RMarkdownChunk[] = _getChunks(), export async function runCurrentAndBelowChunks(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): Promise { const currentChunk = getCurrentChunk(chunks, line); + if (!currentChunk) { + return; + } const currentChunkId = currentChunk.id; const lastChunkId = chunks.length; @@ -375,7 +410,9 @@ export async function runCurrentAndBelowChunks(chunks: RMarkdownChunk[] = _getCh for (let i = currentChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); - codeRanges.push(chunk.codeRange); + if (chunk) { + codeRanges.push(chunk.codeRange); + } } await runChunksInTerm(codeRanges); } @@ -389,7 +426,7 @@ export async function runAllChunks(chunks: RMarkdownChunk[] = _getChunks()): Pro for (let i = firstChunkId; i <= lastChunkId; i++) { const chunk = chunks.find(e => e.id === i); - if (chunk.eval) { + if (chunk?.eval) { codeRanges.push(chunk.codeRange); } } @@ -399,26 +436,37 @@ export async function runAllChunks(chunks: RMarkdownChunk[] = _getChunks()): Pro async function goToChunk(chunk: RMarkdownChunk) { // Move cursor 1 line below 'chunk start line' const line = chunk.startLine + 1; - vscode.window.activeTextEditor.selection = new vscode.Selection(line, 0, line, 0); + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + editor.selection = new vscode.Selection(line, 0, line, 0); await vscode.commands.executeCommand('revealLine', { lineNumber: line, at: 'center' }); } export function goToPreviousChunk(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): void { const previousChunk = getPreviousChunk(chunks, line); - void goToChunk(previousChunk); + if (previousChunk) { + void goToChunk(previousChunk); + } } export function goToNextChunk(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): void { const nextChunk = getNextChunk(chunks, line); - void goToChunk(nextChunk); + if (nextChunk) { + void goToChunk(nextChunk); + } } export function selectCurrentChunk(chunks: RMarkdownChunk[] = _getChunks(), line: number = _getStartLine()): void { const editor = vscode.window.activeTextEditor; const currentChunk = getCurrentChunk__CursorWithinChunk(chunks, line); + if (!editor || !currentChunk) { + return; + } const lines = editor.document.getText().split(/\r?\n/); editor.selection = new vscode.Selection( @@ -451,7 +499,7 @@ export class RMarkdownCompletionItemProvider implements vscode.CompletionItemPro }); } - public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] { + public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] | undefined { const line = document.lineAt(position).text; if (isChunkStartLine(line, false) && getChunkLanguage(line) === 'r') { return this.chunkOptionCompletionItems; diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index d3279d6f6..0596a1a10 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -1,15 +1,15 @@ import * as util from '../util'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; -import path = require('path'); -import yaml = require('js-yaml'); +import * as path from 'path'; +import * as yaml from 'js-yaml'; import { RMarkdownManager, KnitWorkingDirectory } from './manager'; import { runTextInTerm } from '../rTerminal'; import { extensionContext, rmdPreviewManager } from '../extension'; import { DisposableProcess } from '../util'; -export let knitDir: KnitWorkingDirectory = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; +export let knitDir: KnitWorkingDirectory | undefined = util.config().get('rmarkdown.knit.defaults.knitWorkingDirectory') ?? undefined; interface IKnitQuickPickItem { label: string, @@ -27,11 +27,14 @@ interface IYamlFrontmatter { } export class RMarkdownKnitManager extends RMarkdownManager { - private async renderDocument(rDocumentPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string): Promise { + private async renderDocument(rDocumentPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string): Promise { const openOutfile: boolean = util.config().get('rmarkdown.knit.openOutputFile') ?? false; const knitWorkingDir = this.getKnitDir(knitDir, docPath); const knitWorkingDirText = knitWorkingDir ? `${knitWorkingDir}` : ''; const knitCommand = await this.getKnitCommand(yamlParams, rDocumentPath, outputFormat); + if (!knitCommand) { + return; + } this.rPath = await util.getRpath(); const lim = '<<>>'; @@ -64,7 +67,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { return await this.knitWithProgress( { - workingDirectory: knitWorkingDir, + workingDirectory: knitWorkingDirText, fileName: docName, filePath: rDocumentPath, scriptArgs: scriptValues, @@ -99,17 +102,17 @@ export class RMarkdownKnitManager extends RMarkdownManager { } } - let yamlText: string = undefined; + let yamlText: string | undefined = undefined; if (startLine + 1 < endLine) { yamlText = lines.slice(startLine + 1, endLine).join('\n'); } - let paramObj = {}; + let paramObj: IYamlFrontmatter = {}; if (yamlText) { try { paramObj = yaml.load( yamlText - ); + ) as IYamlFrontmatter; } catch (e) { console.error(`Could not parse YAML frontmatter for "${docPath}". Error: ${String(e)}`); } @@ -118,7 +121,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { return paramObj; } - private async getKnitCommand(yamlParams: IYamlFrontmatter, docPath: string, outputFormat: string): Promise { + private async getKnitCommand(yamlParams: IYamlFrontmatter, docPath: string, outputFormat?: string): Promise { let knitCommand: string; if (!yamlParams?.['site']) { @@ -138,6 +141,9 @@ export class RMarkdownKnitManager extends RMarkdownManager { `rmarkdown::render_site(${docPath})`; } else { const cmd = util.config().get('rmarkdown.knit.command'); + if (!cmd) { + return; + } knitCommand = outputFormat ? `${cmd}(${docPath}, output_format = '${outputFormat}')` : `${cmd}(${docPath})`; @@ -150,7 +156,10 @@ export class RMarkdownKnitManager extends RMarkdownManager { // the definition of what constitutes an R Markdown site differs // depending on the type of R Markdown site (i.e., "simple" vs. blogdown sites) private async findSiteParam(): Promise { - const wad = vscode.window.activeTextEditor.document.uri.fsPath; + const wad = vscode.window.activeTextEditor?.document.uri.fsPath; + if (!wad) { + return; + } const rootFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? path.dirname(wad); const indexFile = (await vscode.workspace.findFiles(new vscode.RelativePattern(rootFolder, 'index.{Rmd,rmd, md}'), null, 1))?.[0]; const siteRoot = path.join(path.dirname(wad), '_site.yml'); @@ -176,8 +185,9 @@ export class RMarkdownKnitManager extends RMarkdownManager { // alters the working directory for evaluating chunks public setKnitDir(): void { - const currentDocumentWorkspacePath: string = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor?.document?.uri)?.uri?.fsPath; - const currentDocumentFolderPath: string = path.dirname(vscode.window?.activeTextEditor.document?.uri?.fsPath); + const textEditor = vscode.window.activeTextEditor; + const currentDocumentWorkspacePath: string | undefined = textEditor ? vscode.workspace.getWorkspaceFolder(textEditor.document.uri)?.uri.fsPath : undefined; + const currentDocumentFolderPath: string | undefined = textEditor ? path.dirname(textEditor.document.uri.fsPath) : undefined; const items: IKnitQuickPickItem[] = []; if (currentDocumentWorkspacePath) { @@ -213,7 +223,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { ).then(async choice => { if (choice?.value && knitDir !== choice.value) { knitDir = choice.value; - await rmdPreviewManager.updatePreview(); + await rmdPreviewManager?.updatePreview(); } }); } else { @@ -223,13 +233,19 @@ export class RMarkdownKnitManager extends RMarkdownManager { } public async knitRmd(echo: boolean, outputFormat?: string): Promise { - const wad: vscode.TextDocument = vscode.window.activeTextEditor.document; + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + void vscode.window.showWarningMessage('No text editor active.'); + return; + } + + const wad: vscode.TextDocument = textEditor.document; // handle untitled rmd - if (vscode.window.activeTextEditor.document.isUntitled) { + if (textEditor.document.isUntitled) { void vscode.window.showWarningMessage('Cannot knit an untitled file. Please save the document.'); await vscode.commands.executeCommand('workbench.action.files.save').then(() => { - if (!vscode.window.activeTextEditor.document.isUntitled) { + if (!textEditor.document.isUntitled) { void this.knitRmd(echo, outputFormat); } }); @@ -248,7 +264,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { // allow users to opt out of background process if (util.config().get('rmarkdown.knit.useBackgroundProcess')) { - const busyPath = wad.uri.fsPath + outputFormat; + const busyPath = wad.uri.fsPath + (outputFormat ?? ''); if (this.busyUriStore.has(busyPath)) { return; } diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index d178c299d..6bcd9b983 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -24,18 +24,18 @@ interface IKnitArgs { scriptPath: string; rCmd?: string; rOutputFormat?: string; - callback: (...args: unknown[]) => boolean; - onRejection?: (...args: unknown[]) => unknown; + callback: (dat: string, childProcess?: util.DisposableProcess) => boolean; + onRejection?: (filePath: string, rejection: IKnitRejection) => unknown; } export abstract class RMarkdownManager { - protected rPath: string = undefined; + protected rPath: string | undefined = undefined; protected rMarkdownOutput: vscode.OutputChannel = rMarkdownOutput; // uri that are in the process of knitting // so that we can't spam the knit/preview button protected busyUriStore: Set = new Set(); - protected getKnitDir(knitDir: string, docPath: string): string { + protected getKnitDir(knitDir: string | undefined, docPath: string): string | undefined { switch (knitDir) { // the directory containing the R Markdown document case KnitWorkingDirectory.documentDirectory: { @@ -89,19 +89,24 @@ export abstract class RMarkdownManager { cwd: args.workingDirectory, }; - let childProcess: DisposableProcess; + let childProcess: DisposableProcess | undefined = undefined; try { + if (!this.rPath) { + throw new Error('R path not defined'); + } + childProcess = spawn(this.rPath, cpArgs, processOptions, () => { rMarkdownOutput.appendLine('[VSC-R] terminating R process'); printOutput = false; }); - progress.report({ + progress?.report({ increment: 0, message: '0%' }); } catch (e: unknown) { console.warn(`[VSC-R] error: ${e as string}`); reject({ cp: childProcess, wasCancelled: false }); + return; } this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process started`); @@ -123,7 +128,7 @@ export abstract class RMarkdownManager { if (percentRegOutput) { for (const item of percentRegOutput) { const perc = Number(item); - progress.report( + progress?.report( { increment: perc - currentProgress, message: `${perc}%` @@ -133,10 +138,14 @@ export abstract class RMarkdownManager { } } if (token?.isCancellationRequested) { - resolve(childProcess); + if (childProcess) { + resolve(childProcess); + } } else { if (args.callback(dat, childProcess)) { - resolve(childProcess); + if (childProcess) { + resolve(childProcess); + } } } } @@ -151,7 +160,7 @@ export abstract class RMarkdownManager { childProcess.on('exit', (code, signal) => { this.rMarkdownOutput.appendLine(`[VSC-R] ${fileName} process exited ` + - (signal ? `from signal '${signal}'` : `with exit code ${code}`)); + (signal ? `from signal '${signal}'` : `with exit code ${code || 'null'}`)); if (code !== 0) { reject({ cp: childProcess, wasCancelled: false }); } @@ -164,12 +173,17 @@ export abstract class RMarkdownManager { ); } - protected async knitWithProgress(args: IKnitArgs): Promise { - let childProcess: DisposableProcess = undefined; + protected async knitWithProgress(args: IKnitArgs): Promise { + let childProcess: DisposableProcess | undefined = undefined; await util.doWithProgress( - async (token: vscode.CancellationToken, progress: vscode.Progress) => { + (async ( + token: vscode.CancellationToken | undefined, + progress: vscode.Progress<{ + message?: string | undefined; + increment?: number | undefined; + }> | undefined) => { childProcess = await this.knitDocument(args, token, progress) as DisposableProcess; - }, + }), vscode.ProgressLocation.Notification, `Knitting ${args.fileName} ${args.rOutputFormat ? 'to ' + args.rOutputFormat : ''} `, true diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index cf4112e4e..f3f37c5ef 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -13,17 +13,17 @@ import { RMarkdownManager } from './manager'; class RMarkdownPreview extends vscode.Disposable { title: string; - cp: DisposableProcess; + cp: DisposableProcess | undefined; panel: vscode.WebviewPanel; resourceViewColumn: vscode.ViewColumn; outputUri: vscode.Uri; - htmlDarkContent: string; - htmlLightContent: string; - fileWatcher: fs.FSWatcher; + htmlDarkContent: string | undefined; + htmlLightContent: string | undefined; + fileWatcher: fs.FSWatcher | undefined; autoRefresh: boolean; mtime: number; - constructor(title: string, cp: DisposableProcess, panel: vscode.WebviewPanel, + constructor(title: string, cp: DisposableProcess | undefined, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, filePath: string, RMarkdownPreviewManager: RMarkdownPreviewManager, useCodeTheme: boolean, autoRefresh: boolean) { super(() => { @@ -46,19 +46,19 @@ class RMarkdownPreview extends vscode.Disposable { public styleHtml(useCodeTheme: boolean) { if (useCodeTheme) { - this.panel.webview.html = this.htmlDarkContent; + this.panel.webview.html = this.htmlDarkContent ?? ''; } else { - this.panel.webview.html = this.htmlLightContent; + this.panel.webview.html = this.htmlLightContent ?? ''; } } public async refreshContent(useCodeTheme: boolean) { - this.getHtmlContent(await readContent(this.outputUri.fsPath, 'utf8')); + this.getHtmlContent(await readContent(this.outputUri.fsPath, 'utf8') ?? ''); this.styleHtml(useCodeTheme); } private startFileWatcher(RMarkdownPreviewManager: RMarkdownPreviewManager, filePath: string) { - let fsTimeout: NodeJS.Timeout; + let fsTimeout: NodeJS.Timeout | null; const fileWatcher = fs.watch(filePath, {}, () => { const mtime = fs.statSync(filePath).mtime.getTime(); if (this.autoRefresh && !fsTimeout && mtime !== this.mtime) { @@ -91,7 +91,11 @@ class RMarkdownPreview extends vscode.Disposable { if (chunkCol) { const colReg = /[0-9.]+/g; const regOut = chunkCol.match(colReg); - outCol = `rgba(${regOut[0] ?? 128}, ${regOut[1] ?? 128}, ${regOut[2] ?? 128}, ${Math.max(0, Number(regOut[3] ?? 0.1) - 0.05)})`; + if (regOut) { + outCol = `rgba(${regOut[0] ?? 128}, ${regOut[1] ?? 128}, ${regOut[2] ?? 128}, ${Math.max(0, Number(regOut[3] ?? 0.1) - 0.05)})`; + } else { + outCol = 'rgba(128, 128, 128, 0.05)'; + } } else { chunkCol = 'rgba(128, 128, 128, 0.1)'; outCol = 'rgba(128, 128, 128, 0.05)'; @@ -146,15 +150,15 @@ class RMarkdownPreviewStore extends vscode.Disposable { // dispose child and remove it from set public delete(filePath: string): boolean { - this.store.get(filePath).dispose(); + this.store.get(filePath)?.dispose(); return this.store.delete(filePath); } - public get(filePath: string): RMarkdownPreview { + public get(filePath: string): RMarkdownPreview | undefined { return this.store.get(filePath); } - public getFilePath(preview: RMarkdownPreview): string { + public getFilePath(preview: RMarkdownPreview): string | undefined { for (const _preview of this.store) { if (_preview[1] === preview) { return _preview[0]; @@ -174,7 +178,7 @@ class RMarkdownPreviewStore extends vscode.Disposable { export class RMarkdownPreviewManager extends RMarkdownManager { // the currently selected RMarkdown preview - private activePreview: { filePath: string, preview: RMarkdownPreview, title: string } = { filePath: null, preview: null, title: null }; + private activePreview: { filePath: string | null, preview: RMarkdownPreview | null, title: string | null } = { filePath: null, preview: null, title: null }; // store of all open RMarkdown previews private previewStore: RMarkdownPreviewStore = new RMarkdownPreviewStore; private useCodeTheme = true; @@ -186,15 +190,21 @@ export class RMarkdownPreviewManager extends RMarkdownManager { public async previewRmd(viewer: vscode.ViewColumn, uri?: vscode.Uri): Promise { - const filePath = uri ? uri.fsPath : vscode.window.activeTextEditor.document.uri.fsPath; + const textEditor = vscode.window.activeTextEditor; + if (!textEditor) { + void vscode.window.showErrorMessage('No text editor active.'); + return; + } + + const filePath = uri ? uri.fsPath : textEditor.document.uri.fsPath; const fileName = path.basename(filePath); const currentViewColumn: vscode.ViewColumn = vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.Active ?? vscode.ViewColumn.One; // handle untitled rmd files - if (!uri && vscode.window.activeTextEditor.document.isUntitled) { + if (!uri && textEditor.document.isUntitled) { void vscode.window.showWarningMessage('Cannot knit an untitled file. Please save the document.'); await vscode.commands.executeCommand('workbench.action.files.save').then(() => { - if (!vscode.window.activeTextEditor.document.isUntitled) { + if (!textEditor.document.isUntitled) { void this.previewRmd(viewer); } }); @@ -203,7 +213,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { const isSaved = uri ? true : - await saveDocument(vscode.window.activeTextEditor.document); + await saveDocument(textEditor.document); if (!isSaved) { return; @@ -264,17 +274,17 @@ export class RMarkdownPreviewManager extends RMarkdownManager { } public async openExternalBrowser(): Promise { - if (this.activePreview) { - await vscode.env.openExternal(this.activePreview?.preview?.outputUri); + if (this.activePreview.preview) { + await vscode.env.openExternal(this.activePreview.preview.outputUri); } } public async updatePreview(preview?: RMarkdownPreview): Promise { - const toUpdate = preview ?? this.activePreview?.preview; - const previewUri = this.previewStore?.getFilePath(toUpdate); + const toUpdate = preview ?? this.activePreview.preview; + const previewUri = toUpdate ? this.previewStore.getFilePath(toUpdate) : undefined; toUpdate?.cp?.dispose(); - if (toUpdate) { + if (toUpdate && previewUri) { const childProcess: DisposableProcess | void = await this.previewDocument(previewUri, toUpdate.title).catch(() => { void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); this.rMarkdownOutput.show(true); @@ -290,7 +300,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { } - private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise { + private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise { const knitWorkingDir = this.getKnitDir(knitDir, filePath); const knitWorkingDirText = knitWorkingDir ? `${knitWorkingDir}` : ''; this.rPath = await getRpath(); @@ -307,18 +317,18 @@ export class RMarkdownPreviewManager extends RMarkdownManager { }; - const callback = (dat: string, childProcess: DisposableProcess) => { + const callback = (dat: string, childProcess?: DisposableProcess) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); if (outputUrl) { - if (viewer !== undefined) { - const autoRefresh = config().get('rmarkdown.preview.autoRefresh'); + if (viewer !== undefined && fileName) { + const autoRefresh = config().get('rmarkdown.preview.autoRefresh', false); void this.openPreview( vscode.Uri.file(outputUrl), filePath, fileName, childProcess, viewer, - currentViewColumn, + currentViewColumn ?? vscode.ViewColumn.Active, autoRefresh ); } @@ -333,21 +343,23 @@ export class RMarkdownPreviewManager extends RMarkdownManager { } }; - return await this.knitWithProgress( - { - workingDirectory: knitWorkingDir, - fileName: fileName, - filePath: filePath, - scriptPath: extensionContext.asAbsolutePath('R/rmarkdown/preview.R'), - scriptArgs: scriptValues, - rOutputFormat: 'html preview', - callback: callback, - onRejection: onRejected - } - ); + if (knitWorkingDir && fileName) { + return await this.knitWithProgress( + { + workingDirectory: knitWorkingDir, + fileName: fileName, + filePath: filePath, + scriptPath: extensionContext.asAbsolutePath('R/rmarkdown/preview.R'), + scriptArgs: scriptValues, + rOutputFormat: 'html preview', + callback: callback, + onRejection: onRejected + } + ); + } } - private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: DisposableProcess, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { + private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: DisposableProcess | undefined, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { const panel = vscode.window.createWebviewPanel( 'previewRmd', diff --git a/src/rstudioapi.ts b/src/rstudioapi.ts index 0df21ceca..ac4d738e5 100644 --- a/src/rstudioapi.ts +++ b/src/rstudioapi.ts @@ -116,7 +116,7 @@ export async function documentContext(id: string) { }; } -export async function insertOrModifyText(query: any[], id: string = null) { +export async function insertOrModifyText(query: any[], id: string | null = null) { const target = findTargetUri(id); @@ -218,7 +218,8 @@ export async function documentSaveAll(): Promise { await workspace.saveAll(); } -export function projectPath(): { path: string; } { +// TODO: very similar to ./utils.getCurrentWorkspaceFolder() +export function projectPath(): { path: string | undefined; } { if (typeof workspace.workspaceFolders !== 'undefined') { // Is there a root folder open? @@ -253,7 +254,11 @@ export function projectPath(): { path: string; } { } export async function documentNew(text: string, type: string, position: number[]): Promise { - const documentUri = Uri.parse('untitled:' + path.join(projectPath().path, 'new_document.' + type)); + const currentProjectPath = projectPath().path; + if (!currentProjectPath) { + return; // TODO: Report failure + } + const documentUri = Uri.parse('untitled:' + path.join(currentProjectPath, 'new_document.' + type)); const targetDocument = await workspace.openTextDocument(documentUri); const edit = new WorkspaceEdit(); const docLines = targetDocument.lineCount; @@ -280,7 +285,7 @@ interface AddinItem extends QuickPickItem { package: string; } -let addinQuickPicks: AddinItem[] = undefined; +let addinQuickPicks: AddinItem[] | undefined = undefined; export async function getAddinPickerItems(): Promise { @@ -334,7 +339,7 @@ export async function launchAddinPicker(): Promise { placeHolder: '', onDidSelectItem: undefined }; - const addinSelection: AddinItem = + const addinSelection: AddinItem | undefined = await window.showQuickPick(getAddinPickerItems(), addinPickerOptions); if (!(typeof addinSelection === 'undefined')) { @@ -438,7 +443,7 @@ function getLastActiveTextEditor() { lastActiveTextEditor : window.activeTextEditor); } -function findTargetUri(id: string) { +function findTargetUri(id: string | null) { return (id === null ? getLastActiveTextEditor().document.uri : Uri.parse(id)); } diff --git a/src/selection.ts b/src/selection.ts index 94d213461..3e50c55df 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -4,16 +4,20 @@ import { Position, Range, window } from 'vscode'; import { LineCache } from './lineCache'; -export function getWordOrSelection(): string { - const selection = window.activeTextEditor.selection; - const currentDocument = window.activeTextEditor.document; +export function getWordOrSelection(): string | undefined { + const textEditor = window.activeTextEditor; + if (!textEditor) { + return; + } + const selection = textEditor.selection; + const currentDocument = textEditor.document; let text: string; if ((selection.start.line === selection.end.line) && (selection.start.character === selection.end.character)) { const wordRange = currentDocument.getWordRangeAtPosition(selection.start); text = currentDocument.getText(wordRange); } else { - text = currentDocument.getText(window.activeTextEditor.selection); + text = currentDocument.getText(textEditor.selection); } return text; @@ -39,9 +43,14 @@ export interface RSelection { range: Range; } -export function getSelection(): RSelection { - const currentDocument = window.activeTextEditor.document; - const { start, end } = window.activeTextEditor.selection; +export function getSelection(): RSelection | undefined { + const textEditor = window.activeTextEditor; + if (!textEditor) { + return; + } + + const currentDocument = textEditor.document; + const { start, end } = textEditor.selection; const selection = { linesDownToMoveCursor: 0, selectedText: '', @@ -56,7 +65,7 @@ export function getSelection(): RSelection { (x) => currentDocument.lineAt(x).text, currentDocument.lineCount ); - const charactersOnLine = window.activeTextEditor.document.lineAt(endLine).text.length; + const charactersOnLine = textEditor.document.lineAt(endLine).text.length; const newStart = new Position(startLine, 0); const newEnd = new Position(endLine, charactersOnLine); selection.linesDownToMoveCursor = endLine + 1 - start.line; @@ -74,7 +83,6 @@ export function getSelection(): RSelection { class PositionNeg { public line: number; public character: number; - public cter: number; public constructor(line: number, character: number) { this.line = line; this.character = character; @@ -82,9 +90,8 @@ class PositionNeg { } function doBracketsMatch(a: string, b: string): boolean { - const matches = { '(': ')', '[': ']', '{': '}', ')': '(', ']': '[', '}': '{' }; - - return matches[a] === b; + const matches = new Map(Object.entries({ '(': ')', '[': ']', '{': '}', ')': '(', ']': '[', '}': '{' })); + return matches.get(a) === b; } function isBracket(c: string, lookingForward: boolean) { @@ -216,7 +223,7 @@ export function extendSelection(line: number, getLine: (line: number) => string, getEndsInOperatorFromCache, lineCount ); - poss[Number(lookingForward)] = nextPos; + poss[lookingForward ? 1 : 0] = nextPos; if (quoteChar === '') { if (isQuote(nextChar)) { quoteChar = nextChar; @@ -227,8 +234,8 @@ export function extendSelection(line: number, getLine: (line: number) => string, if (unmatched[lookingForward ? 1 : 0].length === 0) { lookingForward = !lookingForward; unmatched[lookingForward ? 1 : 0].push(nextChar); - flagsFinish[Number(lookingForward)] = false; - } else if (!doBracketsMatch(nextChar, unmatched[lookingForward ? 1 : 0].pop())) { + flagsFinish[lookingForward ? 1 : 0] = false; + } else if (!doBracketsMatch(nextChar, unmatched[lookingForward ? 1 : 0].pop() ?? '')) { flagAbort = true; } } @@ -255,7 +262,7 @@ export function extendSelection(line: number, getLine: (line: number) => string, if (isEndOfCodeLine) { if (unmatched[lookingForward ? 1 : 0].length === 0) { // We have found everything we need to in this direction. Continue looking in the other direction. - flagsFinish[Number(lookingForward)] = true; + flagsFinish[lookingForward ? 1 : 0] = true; lookingForward = !lookingForward; } else if (isEndOfFile) { // Have hit the start or end of the file without finding the matching bracket. diff --git a/src/session.ts b/src/session.ts index ec222180a..f31480d6e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,8 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ 'use strict'; import * as fs from 'fs-extra'; @@ -15,6 +10,7 @@ import { FSWatcher } from 'fs-extra'; import { config, readContent, UriIcon } from './util'; import { purgeAddinPickerItems, dispatchRStudioAPICall } from './rstudioapi'; +import { IRequest } from './liveShare/shareSession'; import { homeExtDir, rWorkspace, globalRHelp, globalHttpgdManager, extensionContext } from './extension'; import { UUID, rHostService, rGuestService, isLiveShare, isHost, isGuestSession, closeBrowser, guestResDir, shareBrowser, openVirtualDoc, shareWorkspace } from './liveShare'; @@ -47,6 +43,7 @@ export let sessionDir: string; export let workingDir: string; let rVer: string; let pid: string; +// eslint-disable-next-line @typescript-eslint/no-explicit-any let info: any; export let workspaceFile: string; let workspaceLockFile: string; @@ -56,9 +53,9 @@ let plotLockFile: string; let plotTimeStamp: number; let workspaceWatcher: FSWatcher; let plotWatcher: FSWatcher; -let activeBrowserPanel: WebviewPanel; -let activeBrowserUri: Uri; -let activeBrowserExternalUri: Uri; +let activeBrowserPanel: WebviewPanel | undefined; +let activeBrowserUri: Uri | undefined; +let activeBrowserExternalUri: Uri | undefined; export function deploySessionWatcher(extensionPath: string): void { console.info(`[deploySessionWatcher] extensionPath: ${extensionPath}`); @@ -96,7 +93,7 @@ export function attachActive(): void { console.info('[attachActive]'); void runTextInTerm('.vsc.attach()'); if (isLiveShare() && shareWorkspace) { - rHostService.notifyRequest(requestFile, true); + rHostService?.notifyRequest(requestFile, true); } } else { void window.showInformationMessage('This command requires that r.sessionWatcher be enabled.'); @@ -182,11 +179,11 @@ async function updatePlot() { void commands.executeCommand('vscode.open', Uri.file(plotFile), { preserveFocus: true, preview: true, - viewColumn: ViewColumn[config().get('session.viewers.viewColumn.plot')], + viewColumn: ViewColumn[(config().get('session.viewers.viewColumn.plot') || 'Two') as keyof typeof ViewColumn], }); console.info('[updatePlot] Done'); if (isLiveShare()) { - void rHostService.notifyPlot(plotFile); + void rHostService?.notifyPlot(plotFile); } } else { console.info('[updatePlot] File not found'); @@ -203,11 +200,11 @@ async function updateWorkspace() { workspaceTimeStamp = newTimeStamp; if (fs.existsSync(workspaceFile)) { const content = await fs.readFile(workspaceFile, 'utf8'); - workspaceData = JSON.parse(content); + workspaceData = JSON.parse(content) as WorkspaceData; void rWorkspace?.refresh(); console.info('[updateWorkspace] Done'); if (isLiveShare()) { - rHostService.notifyWorkspace(workspaceData); + rHostService?.notifyWorkspace(workspaceData); } } else { console.info('[updateWorkspace] File not found'); @@ -227,7 +224,7 @@ export async function showBrowser(url: string, title: string, viewer: string | b title, { preserveFocus: true, - viewColumn: ViewColumn[String(viewer)], + viewColumn: ViewColumn[String(viewer) as keyof typeof ViewColumn], }, { enableFindWidget: true, @@ -290,7 +287,9 @@ export function refreshBrowser(): void { console.log('[refreshBrowser]'); if (activeBrowserPanel) { activeBrowserPanel.webview.html = ''; - activeBrowserPanel.webview.html = getBrowserHtml(activeBrowserExternalUri); + if (activeBrowserExternalUri) { + activeBrowserPanel.webview.html = getBrowserHtml(activeBrowserExternalUri); + } } } @@ -311,7 +310,7 @@ export async function showWebView(file: string, title: string, viewer: string | const panel = window.createWebviewPanel('webview', title, { preserveFocus: true, - viewColumn: ViewColumn[String(viewer)], + viewColumn: ViewColumn[String(viewer) as keyof typeof ViewColumn], }, { enableScripts: true, @@ -336,7 +335,7 @@ export async function showDataView(source: string, type: string, title: string, const panel = window.createWebviewPanel('dataview', title, { preserveFocus: true, - viewColumn: ViewColumn[viewer], + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], }, { enableScripts: true, @@ -351,7 +350,7 @@ export async function showDataView(source: string, type: string, title: string, const panel = window.createWebviewPanel('dataview', title, { preserveFocus: true, - viewColumn: ViewColumn[viewer], + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], }, { enableScripts: true, @@ -364,13 +363,15 @@ export async function showDataView(source: string, type: string, title: string, panel.webview.html = content; } else { if (isGuestSession) { - const fileContent = await rGuestService.requestFileContent(file, 'utf8'); - await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer]); + const fileContent = await rGuestService?.requestFileContent(file, 'utf8'); + if (fileContent) { + await openVirtualDoc(file, fileContent, true, true, ViewColumn[viewer as keyof typeof ViewColumn]); + } } else { await commands.executeCommand('vscode.open', Uri.file(file), { preserveFocus: true, preview: true, - viewColumn: ViewColumn[viewer], + viewColumn: ViewColumn[viewer as keyof typeof ViewColumn], }); } } @@ -379,7 +380,7 @@ export async function showDataView(source: string, type: string, title: string, export async function getTableHtml(webview: Webview, file: string): Promise { resDir = isGuestSession ? guestResDir : resDir; - const pageSize = config().get('session.data.pageSize'); + const pageSize = config().get('session.data.pageSize') || 500; const content = await readContent(file, 'utf8'); return ` @@ -640,7 +641,7 @@ export async function getListHtml(webview: Webview, file: string): Promise { const observerPath = Uri.file(path.join(webviewDir, 'observer.js')); - const body = (await readContent(file, 'utf8')).toString() + const body = (await readContent(file, 'utf8') || '').toString() .replace(/<(\w+)(.*)\s+(href|src)="(?!\w+:)/g, `<$1 $2 $3="${String(webview.asWebviewUri(Uri.file(dir)))}/`); @@ -725,6 +726,10 @@ export async function writeSuccessResponse(responseSessionDir: string): Promise< await writeResponse({ result: true }, responseSessionDir); } +type ISessionRequest = { + plot_url?: string, +} & IRequest; + async function updateRequest(sessionStatusBarItem: StatusBarItem) { console.info('[updateRequest] Started'); console.info(`[updateRequest] requestFile: ${requestFile}`); @@ -734,12 +739,12 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { requestTimeStamp = newTimeStamp; const requestContent = await fs.readFile(requestFile, 'utf8'); console.info(`[updateRequest] request: ${requestContent}`); - const request = JSON.parse(requestContent); - if (isFromWorkspace(request.wd)) { + const request = JSON.parse(requestContent) as ISessionRequest; + if (request.wd && isFromWorkspace(request.wd)) { if (request.uuid === null || request.uuid === undefined || request.uuid === UUID) { switch (request.command) { case 'help': { - if (globalRHelp) { + if (globalRHelp && request.requestPath) { console.log(request.requestPath); await globalRHelp.showHelpForPath(request.requestPath, request.viewer); } @@ -752,6 +757,9 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { break; } case 'attach': { + if (!request.tempdir || !request.wd) { + return; + } rVer = String(request.version); pid = String(request.pid); info = request.info; @@ -759,7 +767,8 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { workingDir = request.wd; console.info(`[updateRequest] attach PID: ${pid}`); sessionStatusBarItem.text = `R ${rVer}: ${pid}`; - sessionStatusBarItem.tooltip = `${info.version}\nProcess ID: ${pid}\nCommand: ${info.command}\nStart time: ${info.start_time}\nClick to attach to active terminal.`; + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access + sessionStatusBarItem.tooltip = `${info?.version}\nProcess ID: ${pid}\nCommand: ${info?.command}\nStart time: ${info?.start_time}\nClick to attach to active terminal.`; sessionStatusBarItem.show(); updateSessionWatcher(); purgeAddinPickerItems(); @@ -769,20 +778,28 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { break; } case 'browser': { - await showBrowser(request.url, request.title, request.viewer); + if (request.url && request.title && request.viewer) { + await showBrowser(request.url, request.title, request.viewer); + } break; } case 'webview': { - await showWebView(request.file, request.title, request.viewer); + if (request.file && request.title && request.viewer) { + await showWebView(request.file, request.title, request.viewer); + } break; } case 'dataview': { - await showDataView(request.source, - request.type, request.title, request.file, request.viewer); + if (request.source && request.type && request.file && request.title && request.viewer) { + await showDataView(request.source, + request.type, request.title, request.file, request.viewer); + } break; } case 'rstudioapi': { - await dispatchRStudioAPICall(request.action, request.args, request.sd); + if (request.action && request.args && request.sd) { + await dispatchRStudioAPICall(request.action, request.args, request.sd); + } break; } default: @@ -793,7 +810,7 @@ async function updateRequest(sessionStatusBarItem: StatusBarItem) { console.info(`[updateRequest] Ignored request outside workspace`); } if (isLiveShare()) { - void rHostService.notifyRequest(requestFile); + void rHostService?.notifyRequest(requestFile); } } } diff --git a/src/tasks.ts b/src/tasks.ts index c839ecdb2..fdb939139 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -101,7 +101,7 @@ function asRTask(rPath: string, folder: vscode.WorkspaceFolder | vscode.TaskScop const rtask: vscode.Task = new vscode.Task( info.definition, folder, - info.name, + info.name ?? 'Unnamed', info.definition.type, new vscode.ProcessExecution( rPath, @@ -131,6 +131,9 @@ export class RTaskProvider implements vscode.TaskProvider { const tasks: vscode.Task[] = []; const rPath = await getRpath(false); + if (!rPath) { + return []; + } for (const folder of folders) { const isRPackage = fs.existsSync(path.join(folder.uri.fsPath, 'DESCRIPTION')); @@ -151,6 +154,9 @@ export class RTaskProvider implements vscode.TaskProvider { name: task.name }; const rPath = await getRpath(false); + if (!rPath) { + throw 'R path not set.'; + } return asRTask(rPath, vscode.TaskScope.Workspace, taskInfo); } } diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 438e5c8c9..439d4cdc6 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -1,7 +1,10 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ + import * as path from 'path'; import * as Mocha from 'mocha'; +// @ts-ignore: all import * as glob from 'glob'; export function run(): Promise { @@ -14,6 +17,7 @@ export function run(): Promise { const testsRoot = path.resolve(__dirname, '..'); return new Promise((c, e) => { + // @ts-ignore: all glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { if (err) { return e(err); diff --git a/src/test/suite/syntax.test.ts b/src/test/suite/syntax.test.ts index b3ea8e490..e1ac81113 100644 --- a/src/test/suite/syntax.test.ts +++ b/src/test/suite/syntax.test.ts @@ -53,6 +53,7 @@ suite('Syntax Highlighting', () => { const re = new RegExp(function_pattern_fixed); const line = 'x <- function(x) {'; const match = re.exec(line); + assert.ok(match); assert.strictEqual(match[3], 'function'); }); @@ -60,6 +61,7 @@ suite('Syntax Highlighting', () => { const re = new RegExp(function_pattern_fixed); const line = 'x <- function (x) {'; const match = re.exec(line); + assert.ok(match); assert.strictEqual(match[3], 'function'); }); diff --git a/src/util.ts b/src/util.ts index 22a9cb648..b2befad21 100644 --- a/src/util.ts +++ b/src/util.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import * as cp from 'child_process'; import { rGuestService, isGuestSession } from './liveShare'; import { extensionContext } from './extension'; +import { randomBytes } from 'crypto'; export function config(): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration('r'); @@ -22,7 +23,7 @@ function getRfromEnvPath(platform: string) { fileExtension = '.exe'; } - const os_paths: string[] | string = process.env.PATH.split(splitChar); + const os_paths: string[] | string = process.env.PATH ? process.env.PATH.split(splitChar) : []; for (const os_path of os_paths) { const os_r_path: string = path.join(os_path, 'R' + fileExtension); if (fs.existsSync(os_r_path)) { @@ -67,8 +68,8 @@ export function getRPathConfigEntry(term: boolean = false): string { return `${trunc}.${platform}`; } -export async function getRpath(quote = false, overwriteConfig?: string): Promise { - let rpath = ''; +export async function getRpath(quote = false, overwriteConfig?: string): Promise { + let rpath: string | undefined = ''; // try the config entry specified in the function arg: if (overwriteConfig) { @@ -146,7 +147,7 @@ export function checkIfFileExists(filePath: string): boolean { return existsSync(filePath); } -export function getCurrentWorkspaceFolder(): vscode.WorkspaceFolder { +export function getCurrentWorkspaceFolder(): vscode.WorkspaceFolder | undefined { if (vscode.workspace.workspaceFolders !== undefined) { if (vscode.workspace.workspaceFolders.length === 1) { return vscode.workspace.workspaceFolders[0]; @@ -168,11 +169,11 @@ export function getCurrentWorkspaceFolder(): vscode.WorkspaceFolder { // // If it is a guest, the guest service requests the host // to read the file, and pass back its contents to the guest -export function readContent(file: PathLike | number): Promise; -export function readContent(file: PathLike | number, encoding: string): Promise; -export function readContent(file: PathLike | number, encoding?: string): Promise { +export function readContent(file: PathLike | number): Promise | undefined; +export function readContent(file: PathLike | number, encoding: string): Promise | undefined; +export function readContent(file: PathLike | number, encoding?: string): Promise | undefined { if (isGuestSession) { - return encoding === undefined ? rGuestService.requestFileContent(file) : rGuestService.requestFileContent(file, encoding); + return encoding === undefined ? rGuestService?.requestFileContent(file) : rGuestService?.requestFileContent(file, encoding); } else { return encoding === undefined ? readFile(file) : readFile(file, encoding); } @@ -223,16 +224,20 @@ export async function executeAsTask(name: string, cmdOrProcess: string, args?: s let taskExecution: vscode.ShellExecution | vscode.ProcessExecution; if(asProcess){ taskDefinition = { type: 'process'}; - taskExecution = new vscode.ProcessExecution( + taskExecution = args ? new vscode.ProcessExecution( cmdOrProcess, args + ) : new vscode.ProcessExecution( + cmdOrProcess ); - } else{ + } else { taskDefinition = { type: 'shell' }; - const quotedArgs = args.map(arg => { return { value: arg, quoting: vscode.ShellQuoting.Weak }; }); - taskExecution = new vscode.ShellExecution( + const quotedArgs = args && args.map(arg => { return { value: arg, quoting: vscode.ShellQuoting.Weak }; }); + taskExecution = quotedArgs ? new vscode.ShellExecution( cmdOrProcess, quotedArgs + ) : new vscode.ShellExecution( + cmdOrProcess ); } const task = new vscode.Task( @@ -259,22 +264,19 @@ export async function executeAsTask(name: string, cmdOrProcess: string, args?: s // executes a callback and shows a 'busy' progress bar during the execution // synchronous callbacks are converted to async to properly render the progress bar // default location is in the help pages tree view -export async function doWithProgress(cb: (token?: vscode.CancellationToken, progress?: vscode.Progress) => T | Promise, location: (string | vscode.ProgressLocation) = vscode.ProgressLocation.Window, title?: string, cancellable?: boolean): Promise { +export async function doWithProgress(cb: (token?: vscode.CancellationToken, progress?: vscode.Progress<{ message?: string; increment?: number }>) => T | Promise, location: (string | vscode.ProgressLocation) = vscode.ProgressLocation.Window, title?: string, cancellable?: boolean): Promise { const location2 = (typeof location === 'string' ? { viewId: location } : location); const options: vscode.ProgressOptions = { location: location2, cancellable: cancellable ?? false, title: title }; - let ret: T; - await vscode.window.withProgress(options, async (progress, token) => { - const retPromise = new Promise((resolve) => setTimeout(() => { + return await vscode.window.withProgress(options, async (progress, token) => { + return await new Promise((resolve) => setTimeout(() => { const ret = cb(token, progress); resolve(ret); })); - ret = await retPromise; }); - return ret; } // get the URL of a CRAN website @@ -294,8 +296,8 @@ export async function getCranUrl(path: string = '', cwd?: string | URL): Promise return url; } -export function getRLibPaths(): string { - return config().get('libPaths').join('\n'); +export function getRLibPaths(): string | undefined { + return config().get('libPaths')?.join('\n'); } // executes an R command returns its output to stdout @@ -307,6 +309,9 @@ export function getRLibPaths(): string { // export async function executeRCommand(rCommand: string, cwd?: string | URL, fallback?: string | ((e: Error) => string)): Promise { const rPath = await getRpath(); + if (!rPath) { + return undefined; + } const options: cp.CommonOptions = { cwd: cwd, @@ -323,7 +328,7 @@ export async function executeRCommand(rCommand: string, cwd?: string | URL, fall '-e', `cat('${lim}')` ]; - let ret: string = undefined; + let ret: string | undefined = undefined; try { const result = await spawnAsync(rPath, args, options); @@ -332,13 +337,13 @@ export async function executeRCommand(rCommand: string, cwd?: string | URL, fall } const re = new RegExp(`${lim}(.*)${lim}`, 'ms'); const match = re.exec(result.stdout); - if (match.length !== 2) { + if (!match || match.length !== 2) { throw new Error('Could not parse R output.'); } ret = match[1]; } catch (e) { if (fallback) { - ret = (typeof fallback === 'function' ? fallback((e instanceof Error) ? e : undefined) : fallback); + ret = (typeof fallback === 'function' ? fallback(catchAsError(e)) : fallback); } else { console.warn(e); } @@ -429,19 +434,21 @@ export function asDisposable(toDispose: T, disposeFunction: (...args: unknown export type DisposableProcess = cp.ChildProcessWithoutNullStreams & vscode.Disposable; export function spawn(command: string, args?: ReadonlyArray, options?: cp.CommonOptions, onDisposed?: () => unknown): DisposableProcess { const proc = cp.spawn(command, args, options); - console.log(`Process ${proc.pid} spawned`); + console.log(proc.pid ? `Process ${proc.pid} spawned` : 'Process failed to spawn'); let running = true; const exitHandler = () => { running = false; - console.log(`Process ${proc.pid} exited`); + console.log(`Process ${proc.pid || ''} exited`); }; proc.on('exit', exitHandler); proc.on('error', exitHandler); const disposable = asDisposable(proc, () => { if (running) { - console.log(`Process ${proc.pid} terminating`); + console.log(`Process ${proc.pid || ''} terminating`); if (process.platform === 'win32') { - cp.spawnSync('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']); + if (proc.pid !== undefined) { + cp.spawnSync('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']); + } } else { proc.kill('SIGKILL'); } @@ -455,18 +462,22 @@ export function spawn(command: string, args?: ReadonlyArray, options?: c export async function spawnAsync(command: string, args?: ReadonlyArray, options?: cp.CommonOptions, onDisposed?: () => unknown): Promise> { return new Promise((resolve) => { + const result: cp.SpawnSyncReturns = { error: undefined, - pid: undefined, - output: undefined, + pid: -1, + output: [], stdout: '', stderr: '', - status: undefined, - signal: undefined + status: null, + signal: null }; + try { const childProcess = spawn(command, args, options, onDisposed); - result.pid = childProcess.pid; + if (childProcess.pid !== undefined) { + result.pid = childProcess.pid; + } childProcess.stdout?.on('data', (chunk: Buffer) => { result.stdout += chunk.toString(); }); @@ -508,6 +519,10 @@ export async function promptToInstallRPackage(name: string, section: string, cwd if (select === 'Yes') { const repo = await getCranUrl('', cwd); const rPath = await getRpath(); + if (!rPath) { + void vscode.window.showErrorMessage('R path not set', 'OK'); + return; + } const args = ['--silent', '--slave', '--no-save', '--no-restore', '-e', `install.packages('${name}', repos='${repo}')`]; void executeAsTask('Install Package', rPath, args, true); if (postInstallMsg) { @@ -518,3 +533,58 @@ export async function promptToInstallRPackage(name: string, section: string, cwd } }); } + +/** + * Create temporary directory. Will avoid name clashes. Caller must delete directory after use. + * + * @param root Parent folder. + * @param hidden If set to true, directory will be prefixed with a '.' (ignored on windows). + * @returns Path to the temporary directory. + */ +export function createTempDir(root: string, hidden?: boolean): string { + const hidePrefix = (!hidden || process.platform === 'win32') ? '' : '.'; + let tempDir: string; + while (fs.existsSync(tempDir = path.join(root, `${hidePrefix}___temp_${randomBytes(8).toString('hex')}`))) { /* Name clash */ } + fs.mkdirSync(tempDir); + return tempDir; +} + +/** + * Utility function for converting 'unknown' types to errors. + * + * Usage: + * + * ```ts + * try { ... } + * catch (e) { + * const err: Error = catchAsError(e); + * } + * ``` + * @param err + * @param fallbackMessage + * @returns + */ +export function catchAsError(err: unknown, fallbackMessage?: string): Error { + return (err instanceof Error) ? err : Error(fallbackMessage ?? 'Unknown error'); +} + + + +const VIEW_COLUMN_KEYS = Object.keys(vscode.ViewColumn).filter(x => isNaN(parseInt(x))); + +export function asViewColumn(s: string | undefined | vscode.ViewColumn): vscode.ViewColumn | undefined; +export function asViewColumn(s: string | undefined | vscode.ViewColumn, fallback: vscode.ViewColumn): vscode.ViewColumn; +export function asViewColumn(s: string | undefined | vscode.ViewColumn, fallback?: vscode.ViewColumn): vscode.ViewColumn | undefined { + if (!s) { + return fallback; + } + if (typeof s !== 'string') { + // s is already ViewColumn: + return s; + } + if (VIEW_COLUMN_KEYS.includes(s)) { + return vscode.ViewColumn[s as keyof typeof vscode.ViewColumn]; + } + return fallback; +} + diff --git a/src/workspaceViewer.ts b/src/workspaceViewer.ts index 6496fbfbc..d08d09156 100644 --- a/src/workspaceViewer.ts +++ b/src/workspaceViewer.ts @@ -18,14 +18,14 @@ async function populatePackageNodes(): Promise { if (rootNode) { // ensure the pkgRootNode is populated. await rootNode.getChildren(); - await rootNode.pkgRootNode.getChildren(); + await rootNode?.pkgRootNode?.getChildren(); } } function getPackageNode(name: string): PackageNode | undefined { const rootNode = globalRHelp?.treeViewWrapper.helpViewProvider.rootItem; if (rootNode) { - return rootNode.pkgRootNode.children?.find(node => node.label === name); + return rootNode?.pkgRootNode?.children?.find(node => node.label === name); } } @@ -36,7 +36,7 @@ export class WorkspaceDataProvider implements TreeDataProvider { private _onDidChangeTreeData: EventEmitter = new EventEmitter(); public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - public data: WorkspaceData; + public data: WorkspaceData | undefined; public refresh(): void { this.data = isGuestSession ? guestWorkspace : workspaceData; @@ -115,6 +115,8 @@ export class WorkspaceDataProvider implements TreeDataProvider { element.treeLevel + 1 ) ); + } else { + return []; } } else { const treeItems = [this.attachedNamespacesRootItem, this.loadedNamespacesRootItem]; @@ -161,7 +163,7 @@ export class WorkspaceDataProvider implements TreeDataProvider { } else if (a.priority < b.priority) { return 1; } else { - return a.label.localeCompare(b.label); + return (a.label && b.label) ? a.label.localeCompare(b.label) : 0; } } @@ -171,7 +173,7 @@ export class WorkspaceDataProvider implements TreeDataProvider { class PackageItem extends TreeItem { public static command : string = 'r.workspaceViewer.package.showQuickPick'; - public label: string; + public label?: string; public name: string; public pkgNode?: PackageNode; public constructor(label: string, name: string, pkgNode?: PackageNode) { @@ -197,8 +199,8 @@ enum TreeLevel { } export class GlobalEnvItem extends TreeItem { - public label: string; - public desc: string; + public label?: string; + public desc?: string; public str: string; public type: string; public treeLevel: number; @@ -234,7 +236,7 @@ export class GlobalEnvItem extends TreeItem { this.contextValue = treeLevel === 0 ? 'rootNode' : `childNode${this.treeLevel}`; } - private getDescription(dim: number[], str: string, rClass: string, type: string): string { + private getDescription(dim: number[] | undefined, str: string, rClass: string, type: string): string { if (dim && type === 'list') { if (dim[1] === 1) { return `${rClass}: ${dim[0]} obs. of ${dim[1]} variable`; diff --git a/tsconfig.json b/tsconfig.json index 537ccae08..63715e44e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,11 @@ "ES2021" ], "sourceMap": true, - // "strictNullChecks": true, - "rootDir": "src" + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true }, "exclude": [ "html", diff --git a/yarn.lock b/yarn.lock index b2443c48a..cba567925 100644 --- a/yarn.lock +++ b/yarn.lock @@ -227,6 +227,14 @@ dependencies: "@types/node" "*" +"@types/glob@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.0.0.tgz#321607e9cbaec54f687a0792b2d1d370739455d2" + integrity sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/js-yaml@^4.0.2": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.3.tgz#9f33cd6fbf0d5ec575dc8c8fc69c7fec1b4eb200" @@ -247,6 +255,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/mocha@^8.2.2": version "8.2.2" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.2.tgz#91daa226eb8c2ff261e6a8cbf8c7304641e095e0"