Skip to content

Commit

Permalink
Use saveEditor proposed API for running untitled Python files (#21183)
Browse files Browse the repository at this point in the history
Closes #21182
  • Loading branch information
Kartik Raj authored May 4, 2023
1 parent 4112b04 commit 6bdada0
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 18 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"envShellEvent",
"testObserver",
"quickPickItemTooltip",
"envCollectionWorkspace"
"envCollectionWorkspace",
"saveEditor"
],
"author": {
"name": "Microsoft Corporation"
Expand Down
10 changes: 10 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,16 @@ export interface IWorkspaceService {
* @return A promise that resolves to a {@link TextDocument document}.
*/
openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument>;
/**
* Saves the editor identified by the given resource to a new file name as provided by the user and
* returns the resulting resource or `undefined` if save was not successful or cancelled.
*
* **Note** that an editor with the provided resource must be opened in order to be saved as.
*
* @param uri the associated uri for the opened editor to save as.
* @return A thenable that resolves when the save-as operation has finished.
*/
saveAs(uri: Uri): Thenable<Uri | undefined>;
}

export const ITerminalManager = Symbol('ITerminalManager');
Expand Down
10 changes: 10 additions & 0 deletions src/client/common/application/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,14 @@ export class WorkspaceService implements IWorkspaceService {
const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true);
return `{${enabledSearchExcludes.join(',')}}`;
}

public async saveAs(uri: Uri): Promise<Uri | undefined> {
try {
// This is a proposed API hence putting it inside try...catch.
const result = await workspace.saveAs(uri);
return result;
} catch (ex) {
return undefined;
}
}
}
10 changes: 3 additions & 7 deletions src/client/terminals/codeExecution/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import '../../common/extensions';
import { inject, injectable } from 'inversify';
import { l10n, Position, Range, TextEditor, Uri } from 'vscode';

import { IApplicationShell, ICommandManager, IDocumentManager } from '../../common/application/types';
import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types';
import { PYTHON_LANGUAGE } from '../../common/constants';
import * as internalScripts from '../../common/process/internal/scripts';
import { IProcessServiceFactory } from '../../common/process/types';
Expand Down Expand Up @@ -123,12 +123,8 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
public async saveFileIfDirty(file: Uri): Promise<Resource> {
const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path);
if (docs.length === 1 && docs[0].isDirty) {
const deferred = createDeferred<Uri>();
this.documentManager.onDidSaveTextDocument((e) => deferred.resolve(e.uri));
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
await commandManager.executeCommand('workbench.action.files.save', file);
const savedFileUri = await deferred.promise;
return savedFileUri;
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
return workspaceService.saveAs(docs[0].uri);
}
return undefined;
}
Expand Down
23 changes: 13 additions & 10 deletions src/test/terminals/codeExecution/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import { SemVer } from 'semver';
import * as TypeMoq from 'typemoq';
import { EventEmitter, Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode';
import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types';
import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode';
import {
IApplicationShell,
ICommandManager,
IDocumentManager,
IWorkspaceService,
} from '../../../client/common/application/types';
import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants';
import '../../../client/common/extensions';
import { ProcessService } from '../../../client/common/process/proc';
Expand Down Expand Up @@ -38,6 +43,7 @@ suite('Terminal - Code Execution Helper', () => {
let processService: TypeMoq.IMock<IProcessService>;
let interpreterService: TypeMoq.IMock<IInterpreterService>;
let commandManager: TypeMoq.IMock<ICommandManager>;
let workspaceService: TypeMoq.IMock<IWorkspaceService>;
const workingPython: PythonEnvironment = {
path: PYTHON_PATH,
version: new SemVer('3.6.6-final'),
Expand All @@ -51,6 +57,7 @@ suite('Terminal - Code Execution Helper', () => {
setup(() => {
const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
commandManager = TypeMoq.Mock.ofType<ICommandManager>();
workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>();
documentManager = TypeMoq.Mock.ofType<IDocumentManager>();
applicationShell = TypeMoq.Mock.ofType<IApplicationShell>();
const envVariablesProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>();
Expand All @@ -69,6 +76,9 @@ suite('Terminal - Code Execution Helper', () => {
envVariablesProvider
.setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny()))
.returns(() => Promise.resolve({}));
serviceContainer
.setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService)))
.returns(() => workspaceService.object);
serviceContainer
.setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny()))
.returns(() => processServiceFactory.object);
Expand Down Expand Up @@ -367,20 +377,13 @@ suite('Terminal - Code Execution Helper', () => {
.setup((d) => d.textDocuments)
.returns(() => [document.object])
.verifiable(TypeMoq.Times.once());
const saveEmitter = new EventEmitter<TextDocument>();
documentManager.setup((d) => d.onDidSaveTextDocument).returns(() => saveEmitter.event);
document.setup((doc) => doc.isUntitled).returns(() => true);
document.setup((doc) => doc.isDirty).returns(() => true);
document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE);
const untitledUri = Uri.file('Untitled-1');
document.setup((doc) => doc.uri).returns(() => untitledUri);
const savedDocument = TypeMoq.Mock.ofType<TextDocument>();
const expectedSavedUri = Uri.file('one.py');
savedDocument.setup((doc) => doc.uri).returns(() => expectedSavedUri);
commandManager
.setup((c) => c.executeCommand('workbench.action.files.save', untitledUri))
.callback(() => saveEmitter.fire(savedDocument.object))
.returns(() => Promise.resolve());
workspaceService.setup((w) => w.saveAs(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedSavedUri));

const savedUri = await helper.saveFileIfDirty(untitledUri);

Expand Down
34 changes: 34 additions & 0 deletions typings/vscode-proposed/vscode.proposed.saveEditor.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// https://github.com/microsoft/vscode/issues/178713

declare module 'vscode' {

export namespace workspace {

/**
* Saves the editor identified by the given resource and returns the resulting resource or `undefined`
* if save was not successful.
*
* **Note** that an editor with the provided resource must be opened in order to be saved.
*
* @param uri the associated uri for the opened editor to save.
* @return A thenable that resolves when the save operation has finished.
*/
export function save(uri: Uri): Thenable<Uri | undefined>;

/**
* Saves the editor identified by the given resource to a new file name as provided by the user and
* returns the resulting resource or `undefined` if save was not successful or cancelled.
*
* **Note** that an editor with the provided resource must be opened in order to be saved as.
*
* @param uri the associated uri for the opened editor to save as.
* @return A thenable that resolves when the save-as operation has finished.
*/
export function saveAs(uri: Uri): Thenable<Uri | undefined>;
}
}

0 comments on commit 6bdada0

Please sign in to comment.