Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/4441.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support other variables for notebookFileRoot besides ${workspaceRoot}. Specifically allow things like ${fileDirName} so that the dir of the first file run in the interactive window is used for the current directory.
1 change: 1 addition & 0 deletions news/2 Fixes/7688.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When there's no workspace open, use the directory of the opened file as the root directory for a jupyter session.
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService
await this.messageService.handle(diagnostic, { commandPrompts });
}
protected resolveVariables(pythonPath: string, resource: Uri | undefined): string {
const workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined;
const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined);
const systemVariables = new SystemVariables(resource, undefined, this.workspace);
return systemVariables.resolveAny(pythonPath);
}
private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] {
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class PythonSettings implements IPythonSettings {
// tslint:disable-next-line:cyclomatic-complexity max-func-body-length
protected update(pythonSettings: WorkspaceConfiguration) {
const workspaceRoot = this.workspaceRoot.fsPath;
const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined);
const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace);

// tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion
this.pythonPath = systemVariables.resolveAny(pythonSettings.get<string>('pythonPath'))!;
Expand Down
63 changes: 58 additions & 5 deletions src/client/common/variables/systemVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

'use strict';

import * as Path from 'path';
import { Range, Uri } from 'vscode';

import { IDocumentManager, IWorkspaceService } from '../application/types';
import * as Types from '../utils/sysTypes';
import { IStringDictionary, ISystemVariables } from './types';

/* tslint:disable:rule1 no-any no-unnecessary-callback-wrapper jsdoc-format no-for-in prefer-const no-increment-decrement */

export abstract class AbstractSystemVariables implements ISystemVariables {
abstract class AbstractSystemVariables implements ISystemVariables {

public resolve(value: string): string;
public resolve(value: string[]): string[];
Expand Down Expand Up @@ -93,11 +95,22 @@ export abstract class AbstractSystemVariables implements ISystemVariables {
export class SystemVariables extends AbstractSystemVariables {
private _workspaceFolder: string;
private _workspaceFolderName: string;
private _filePath: string | undefined;
private _lineNumber: number | undefined;
private _selectedText: string | undefined;
private _execPath: string;

constructor(workspaceFolder?: string) {
constructor(file: Uri | undefined, rootFolder: string | undefined, workspace?: IWorkspaceService, documentManager?: IDocumentManager) {
super();
this._workspaceFolder = typeof workspaceFolder === 'string' ? workspaceFolder : __dirname;
const workspaceFolder = workspace && file ? workspace.getWorkspaceFolder(file) : undefined;
this._workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder || __dirname;
this._workspaceFolderName = Path.basename(this._workspaceFolder);
this._filePath = file ? file.fsPath : undefined;
if (documentManager && documentManager.activeTextEditor) {
this._lineNumber = documentManager.activeTextEditor.selection.anchor.line + 1;
this._selectedText = documentManager.activeTextEditor.document.getText(new Range(documentManager.activeTextEditor.selection.start, documentManager.activeTextEditor.selection.end));
}
this._execPath = process.execPath;
Object.keys(process.env).forEach(key => {
(this as any as Record<string, string | undefined>)[`env:${key}`] = (this as any as Record<string, string | undefined>)[`env.${key}`] = process.env[key];
});
Expand All @@ -122,4 +135,44 @@ export class SystemVariables extends AbstractSystemVariables {
public get workspaceFolderBasename(): string {
return this._workspaceFolderName;
}

public get file(): string | undefined {
return this._filePath;
}

public get relativeFile(): string | undefined {
return this.file ? Path.relative(this._workspaceFolder, this.file) : undefined;
}

public get relativeFileDirname(): string | undefined {
return this.relativeFile ? Path.dirname(this.relativeFile) : undefined;
}

public get fileBasename(): string | undefined {
return this.file ? Path.basename(this.file) : undefined;
}

public get fileBasenameNoExtension(): string | undefined {
return this.file ? Path.parse(this.file).name : undefined;
}

public get fileDirname(): string | undefined {
return this.file ? Path.dirname(this.file) : undefined;
}

public get fileExtname(): string | undefined {
return this.file ? Path.extname(this.file) : undefined;
}

public get lineNumber(): number | undefined {
return this._lineNumber;
}

public get selectedText(): string | undefined {
return this._selectedText;
}

public get execPath(): string {
return this._execPath;
}
}
10 changes: 7 additions & 3 deletions src/client/datascience/interactive-common/interactiveBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,12 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
}
}

protected async setLaunchingFile(file: string): Promise<void> {
if (file !== Identifiers.EmptyFileName && this.notebook) {
await this.notebook.setLaunchingFile(file);
}
}

protected getNotebook(): INotebook | undefined {
return this.notebook;
}
Expand Down Expand Up @@ -493,9 +499,7 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
if (this.notebook) {
// Before we try to execute code make sure that we have an initial directory set
// Normally set via the workspace, but we might not have one here if loading a single loose file
if (file !== Identifiers.EmptyFileName) {
await this.notebook.setInitialDirectory(path.dirname(file));
}
await this.setLaunchingFile(file);

if (debug) {
// Attach our debugger
Expand Down
8 changes: 8 additions & 0 deletions src/client/datascience/interactive-ipynb/nativeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
return this._file;
}

protected async setLaunchingFile(_file: string): Promise<void> {
// For the native editor, use our own file as the path
const notebook = this.getNotebook();
if (this.fileSystem.fileExists(this.file.fsPath) && notebook) {
await notebook.setLaunchingFile(this.file.fsPath);
}
}

protected sendCellsToWebView(cells: ICell[]) {
// Filter out sysinfo messages. Don't want to show those
const filtered = cells.filter(c => c.data.cell_type !== 'messages');
Expand Down
40 changes: 28 additions & 12 deletions src/client/datascience/jupyter/jupyterNotebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as uuid from 'uuid/v4';
import { Disposable, Event, EventEmitter, Uri } from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';

import { ILiveShareApi } from '../../common/application/types';
import { ILiveShareApi, IWorkspaceService } from '../../common/application/types';
import { Cancellation, CancellationError } from '../../common/cancellation';
import { traceError, traceInfo, traceWarning } from '../../common/logger';
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
Expand All @@ -36,6 +36,7 @@ import {
INotebookServerLaunchInfo,
InterruptResult
} from '../types';
import { expandWorkingDir } from './jupyterUtils';

class CellSubscriber {
private deferred: Deferred<CellState> = createDeferred<CellState>();
Expand Down Expand Up @@ -139,6 +140,7 @@ export class JupyterNotebookBase implements INotebook {
private ranInitialSetup = false;
private _resource: Uri;
private _disposed: boolean = false;
private _workingDirectory: string | undefined;

constructor(
_liveShare: ILiveShareApi, // This is so the liveshare mixin works
Expand All @@ -149,7 +151,8 @@ export class JupyterNotebookBase implements INotebook {
private launchInfo: INotebookServerLaunchInfo,
private loggers: INotebookExecutionLogger[],
resource: Uri,
private getDisposedError: () => Error
private getDisposedError: () => Error,
private workspace: IWorkspaceService
) {
this.sessionStartTime = Date.now();
this._resource = resource;
Expand Down Expand Up @@ -183,13 +186,11 @@ export class JupyterNotebookBase implements INotebook {
return;
}
this.ranInitialSetup = true;
this._workingDirectory = undefined;

try {
// When we start our notebook initial, change to our workspace or user specified root directory
if (this.launchInfo && this.launchInfo.workingDir && this.launchInfo.connectionInfo.localLaunch) {
traceInfo(`Changing directory for ${this.resource.toString()}`);
await this.changeDirectoryIfPossible(this.launchInfo.workingDir);
}
await this.updateWorkingDirectory();

const settings = this.configService.getSettings().datascience;
const matplobInit = !settings || settings.enablePlotViewer ? CodeSnippits.MatplotLibInitSvg : CodeSnippits.MatplotLibInitPng;
Expand Down Expand Up @@ -249,12 +250,9 @@ export class JupyterNotebookBase implements INotebook {
return deferred.promise;
}

public async setInitialDirectory(directory: string): Promise<void> {
// If we launched local and have no working directory call this on add code to change directory
if (this.launchInfo && !this.launchInfo.workingDir && this.launchInfo.connectionInfo.localLaunch) {
await this.changeDirectoryIfPossible(directory);
this.launchInfo.workingDir = directory;
}
public setLaunchingFile(file: string): Promise<void> {
// Update our working directory if we don't have one set already
return this.updateWorkingDirectory(file);
}

public executeObservable(code: string, file: string, line: number, id: string, silent: boolean = false): Observable<ICell[]> {
Expand Down Expand Up @@ -587,6 +585,24 @@ export class JupyterNotebookBase implements INotebook {
});
}

private async updateWorkingDirectory(launchingFile?: string): Promise<void> {
if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && !this._workingDirectory) {
// See what our working dir is supposed to be
const suggested = this.launchInfo.workingDir;
if (suggested && await fs.pathExists(suggested)) {
// We should use the launch info directory. It trumps the possible dir
this._workingDirectory = suggested;
return this.changeDirectoryIfPossible(this._workingDirectory);
} else if (launchingFile && await fs.pathExists(launchingFile)) {
// Combine the working directory with this file if possible.
this._workingDirectory = expandWorkingDir(this.launchInfo.workingDir, launchingFile, this.workspace);
if (this._workingDirectory) {
return this.changeDirectoryIfPossible(this._workingDirectory);
}
}
}
}

private changeDirectoryIfPossible = async (directory: string): Promise<void> => {
if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && await fs.pathExists(directory)) {
await this.executeSilently(`%cd "${directory}"`);
Expand Down
5 changes: 4 additions & 1 deletion src/client/datascience/jupyter/jupyterServerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Uri } from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';
import * as vsls from 'vsls/vscode';

import { ILiveShareApi } from '../../common/application/types';
import { ILiveShareApi, IWorkspaceService } from '../../common/application/types';
import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../common/types';
import {
IConnection,
Expand All @@ -36,6 +36,7 @@ type JupyterServerClassType = {
disposableRegistry: IDisposableRegistry,
configService: IConfigurationService,
sessionManager: IJupyterSessionManagerFactory,
workspaceService: IWorkspaceService,
loggers: INotebookExecutionLogger[]
): IJupyterServerInterface;
};
Expand All @@ -55,6 +56,7 @@ export class JupyterServerFactory implements INotebookServer, ILiveShareHasRole
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
@inject(IConfigurationService) configService: IConfigurationService,
@inject(IJupyterSessionManagerFactory) sessionManager: IJupyterSessionManagerFactory,
@inject(IWorkspaceService) workspaceService: IWorkspaceService,
@multiInject(INotebookExecutionLogger) @optional() loggers: INotebookExecutionLogger[] | undefined) {
this.serverFactory = new RoleBasedFactory<IJupyterServerInterface, JupyterServerClassType>(
liveShare,
Expand All @@ -66,6 +68,7 @@ export class JupyterServerFactory implements INotebookServer, ILiveShareHasRole
disposableRegistry,
configService,
sessionManager,
workspaceService,
loggers ? loggers : []
);
}
Expand Down
20 changes: 20 additions & 0 deletions src/client/datascience/jupyter/jupyterUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import '../../common/extensions';

import * as path from 'path';
import { Uri } from 'vscode';

import { IWorkspaceService } from '../../common/application/types';
import { SystemVariables } from '../../common/variables/systemVariables';

export function expandWorkingDir(workingDir: string | undefined, launchingFile: string, workspace: IWorkspaceService): string {
if (workingDir) {
const variables = new SystemVariables(Uri.file(launchingFile), undefined, workspace);
return variables.resolve(workingDir);
}

// No working dir, just use the path of the launching file.
return path.dirname(launchingFile);
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class GuestJupyterNotebook
return deferred.promise;
}

public setInitialDirectory(_directory: string): Promise<void> {
public setLaunchingFile(_directory: string): Promise<void> {
// Ignore this command on this side
return Promise.resolve();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Uri } from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';
import * as vsls from 'vsls/vscode';

import { ILiveShareApi } from '../../../common/application/types';
import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types';
import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../../common/types';
import { createDeferred, Deferred } from '../../../common/utils/async';
import * as localize from '../../../common/utils/localize';
Expand Down Expand Up @@ -38,6 +38,7 @@ export class GuestJupyterServer
private disposableRegistry: IDisposableRegistry,
private configService: IConfigurationService,
_sessionManager: IJupyterSessionManagerFactory,
_workspaceService: IWorkspaceService,
_loggers: INotebookExecutionLogger[]
) {
super(liveShare);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as vscode from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';
import * as vsls from 'vsls/vscode';

import { ILiveShareApi } from '../../../common/application/types';
import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types';
import { traceError } from '../../../common/logger';
import { IConfigurationService, IDisposableRegistry } from '../../../common/types';
import { createDeferred } from '../../../common/utils/async';
Expand Down Expand Up @@ -50,9 +50,10 @@ export class HostJupyterNotebook
launchInfo: INotebookServerLaunchInfo,
loggers: INotebookExecutionLogger[],
resource: vscode.Uri,
getDisposedError: () => Error
getDisposedError: () => Error,
workspace: IWorkspaceService
) {
super(liveShare, session, configService, disposableRegistry, owner, launchInfo, loggers, resource, getDisposedError);
super(liveShare, session, configService, disposableRegistry, owner, launchInfo, loggers, resource, getDisposedError, workspace);
}

public dispose = async (): Promise<void> => {
Expand Down
6 changes: 4 additions & 2 deletions src/client/datascience/jupyter/liveshare/hostJupyterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import { CancellationToken } from 'vscode-jsonrpc';
import * as vsls from 'vsls/vscode';

import { ILiveShareApi } from '../../../common/application/types';
import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types';
import { traceInfo } from '../../../common/logger';
import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../../common/types';
import * as localize from '../../../common/utils/localize';
Expand Down Expand Up @@ -46,6 +46,7 @@ export class HostJupyterServer
disposableRegistry: IDisposableRegistry,
configService: IConfigurationService,
sessionManager: IJupyterSessionManagerFactory,
private workspaceService: IWorkspaceService,
loggers: INotebookExecutionLogger[]) {
super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers);
}
Expand Down Expand Up @@ -162,7 +163,8 @@ export class HostJupyterServer
launchInfo,
loggers,
resource,
this.getDisposedError.bind(this));
this.getDisposedError.bind(this),
this.workspaceService);

// Wait for it to be ready
traceInfo(`Waiting for idle ${this.id}`);
Expand Down
5 changes: 4 additions & 1 deletion src/client/datascience/jupyter/liveshare/serverCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class ServerCache implements IAsyncDisposable {
// User setting is absolute and doesn't exist, use workspace
workingDir = workspaceFolderPath;
}
} else {
} else if (!fileRoot.includes('${')) {
// fileRoot is a relative path, combine it with the workspace folder
const combinedPath = path.join(workspaceFolderPath, fileRoot);
if (await this.fileSystem.directoryExists(combinedPath)) {
Expand All @@ -112,6 +112,9 @@ export class ServerCache implements IAsyncDisposable {
// Combined path doesn't exist, use workspace
workingDir = workspaceFolderPath;
}
} else {
// fileRoot is a variable that hasn't been expanded
workingDir = fileRoot;
}
}
return workingDir;
Expand Down
Loading