('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"