Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new Python file command #18522

Merged
merged 19 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from 18 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/18376.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement a "New Python File" command
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"onDebugInitialConfigurations",
"onLanguage:python",
"onDebugResolve:python",
"onCommand:python.createNewFile",
"onCommand:python.execInTerminal",
"onCommand:python.debugInTerminal",
"onCommand:python.sortImports",
Expand Down Expand Up @@ -322,6 +323,13 @@
}
],
"commands": [
{
"title": "%python.command.python.createNewFile.title%",
"shortTitle": "%python.menu.createNewFile.title%",
"category": "Python",
"command": "python.createNewFile",
"when": "config.python.createNewFileEnabled"
luabud marked this conversation as resolved.
Show resolved Hide resolved
},
{
"category": "Python",
"command": "python.analysis.restartLanguageServer",
Expand Down Expand Up @@ -503,6 +511,15 @@
"scope": "machine",
"type": "string"
},
"python.createNewFileEnabled": {
"default": "false",
"description": "Enable the `Python: New Python File` command.",
"scope": "machine",
"type": "boolean",
"tags": [
"experimental"
]
},
"python.defaultInterpreterPath": {
"default": "python",
"description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See https://aka.ms/AAfekmf to understand when this is used.",
Expand Down Expand Up @@ -1863,6 +1880,13 @@
"when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported"
}
],
"file/newFile": [
{
"command": "python.createNewFile",
"category": "file",
"when": "config.python.createNewFileEnabled"
}
],
"view/title": [
{
"command": "python.refreshTests",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"python.command.python.sortImports.title": "Sort Imports",
"python.command.python.startREPL.title": "Start REPL",
"python.command.python.createTerminal.title": "Create Terminal",
"python.command.python.createNewFile.title": "New Python File",
"python.command.python.execInTerminal.title": "Run Python File in Terminal",
"python.command.python.debugInTerminal.title": "Debug Python File",
"python.command.python.execInTerminalIcon.title": "Run Python File",
Expand Down Expand Up @@ -29,6 +30,7 @@
"python.command.python.analysis.restartLanguageServer.title": "Restart Language Server",
"python.command.python.launchTensorBoard.title": "Launch TensorBoard",
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
"python.menu.createNewFile.title": "Python File",
"python.snippet.launch.standard.label": "Python: Current File",
"python.snippet.launch.module.label": "Python: Module",
"python.snippet.launch.module.default": "enter-your-module-name",
Expand Down
11 changes: 11 additions & 0 deletions src/client/common/application/applicationShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ import {
SaveDialogOptions,
StatusBarAlignment,
StatusBarItem,
TextDocument,
TextEditor,
TreeView,
TreeViewOptions,
Uri,
ViewColumn,
window,
WindowState,
WorkspaceFolder,
Expand Down Expand Up @@ -100,6 +103,14 @@ export class ApplicationShell implements IApplicationShell {
public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined> {
return window.showInputBox(options, token);
}
public showTextDocument(
document: TextDocument,
column?: ViewColumn,
preserveFocus?: boolean,
): Thenable<TextEditor> {
return window.showTextDocument(document, column, preserveFocus);
}

public openUrl(url: string): void {
env.openExternal(Uri.parse(url));
}
Expand Down
1 change: 1 addition & 0 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
[Commands.PickLocalProcess]: [];
[Commands.ClearStorage]: [];
[Commands.ReportIssue]: [];
[Commands.CreateNewFile]: [];
[Commands.RefreshTensorBoard]: [];
[LSCommands.RestartLS]: [];
}
Expand Down
31 changes: 31 additions & 0 deletions src/client/common/application/commands/createFileCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { injectable, inject } from 'inversify';
import { IExtensionSingleActivationService } from '../../../activation/types';
import { Commands } from '../../constants';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../types';
import { sendTelemetryEvent } from '../../../telemetry';
import { EventName } from '../../../telemetry/constants';

@injectable()
export class CreatePythonFileCommandHandler implements IExtensionSingleActivationService {
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true };

constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
) {}

public async activate(): Promise<void> {
if (!this.workspaceService.getConfiguration('python').get<boolean>('createNewFileEnabled')) {
return;
}
this.commandManager.registerCommand(Commands.CreateNewFile, this.createPythonFile, this);
}

// eslint-disable-next-line class-methods-use-this
luabud marked this conversation as resolved.
Show resolved Hide resolved
public async createPythonFile(): Promise<void> {
const newFile = await this.workspaceService.openTextDocument({ language: 'python' });
this.appShell.showTextDocument(newFile);
sendTelemetryEvent(EventName.CREATE_NEW_FILE_COMMAND);
}
}
23 changes: 23 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ export interface IApplicationShell {
*/
showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined>;

/**
* Show the given document in a text editor. A {@link ViewColumn column} can be provided
* to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}.
*
* @param document A text document to be shown.
* @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values
* are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@linkcode ViewColumn.Beside}
* to open the editor to the side of the currently active one.
* @param preserveFocus When `true` the editor will not take focus.
* @return A promise that resolves to an {@link TextEditor editor}.
*/
showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable<TextEditor>;

/**
* Creates a [QuickPick](#QuickPick) to let the user pick an item from a list
* of items of type T.
Expand Down Expand Up @@ -833,6 +846,16 @@ export interface IWorkspaceService {
* @return The full configuration or a subset.
*/
getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration;

/**
* Opens an untitled text document. The editor will prompt the user for a file
* path when the document is to be saved. The `options` parameter allows to
* specify the *language* and/or the *content* of the document.
*
* @param options Options to control how the document will be created.
* @return A promise that resolves to a {@link TextDocument document}.
*/
openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument>;
}

export const ITerminalManager = Symbol('ITerminalManager');
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/application/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Event,
FileSystemWatcher,
GlobPattern,
TextDocument,
Uri,
workspace,
WorkspaceConfiguration,
Expand Down Expand Up @@ -97,6 +98,10 @@ export class WorkspaceService implements IWorkspaceService {
return workspace.onDidGrantWorkspaceTrust;
}

public openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument> {
return workspace.openTextDocument(options);
}

private get searchExcludes() {
const searchExcludes = this.getConfiguration('search.exclude');
const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true);
Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export namespace Commands {
export const ViewOutput = 'python.viewOutput';
export const Start_REPL = 'python.startREPL';
export const Create_Terminal = 'python.createTerminal';
export const CreateNewFile = 'python.createNewFile';
export const Set_Linter = 'python.setLinter';
export const Enable_Linter = 'python.enableLinting';
export const Run_Linter = 'python.runLinting';
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ClipboardService } from './application/clipboard';
import { CommandManager } from './application/commandManager';
import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand';
import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand';
import { CreatePythonFileCommandHandler } from './application/commands/createFileCommand';
import { DebugService } from './application/debugService';
import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
import { DocumentManager } from './application/documentManager';
Expand Down Expand Up @@ -198,6 +199,10 @@ export function registerTypes(serviceManager: IServiceManager): void {
IExtensionSingleActivationService,
ReportIssueCommandHandler,
);
serviceManager.addSingleton<IExtensionSingleActivationService>(
IExtensionSingleActivationService,
CreatePythonFileCommandHandler,
);
serviceManager.addSingleton<IExtensionChannelService>(IExtensionChannelService, ExtensionChannelService);
serviceManager.addSingleton<IExtensionChannelRule>(
IExtensionChannelRule,
Expand Down
1 change: 1 addition & 0 deletions src/client/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export enum EventName {

SELECT_LINTER = 'LINTING.SELECT',
USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND',
CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND',

LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT',
HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME',
Expand Down
7 changes: 7 additions & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,13 @@ export interface IEventNamePropertyMapping {
"use_report_issue_command" : { }
*/
[EventName.USE_REPORT_ISSUE_COMMAND]: unknown;
/**
* Telemetry event sent when the New Python File command is executed.
*/
/* __GDPR__
"create_new_file_command" : { }
*/
[EventName.CREATE_NEW_FILE_COMMAND]: unknown;
/**
* Telemetry event sent once on session start with details on which experiments are opted into and opted out from.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
luabud marked this conversation as resolved.
Show resolved Hide resolved
import { TextDocument } from 'vscode';
import { Commands } from '../../../../client/common/constants';
import { CommandManager } from '../../../../client/common/application/commandManager';
import { CreatePythonFileCommandHandler } from '../../../../client/common/application/commands/createFileCommand';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types';
import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig';
import { WorkspaceService } from '../../../../client/common/application/workspace';
import { ApplicationShell } from '../../../../client/common/application/applicationShell';

suite('Create New Python File Commmand', () => {
let createNewFileCommandHandler: CreatePythonFileCommandHandler;
let cmdManager: ICommandManager;
let workspaceService: IWorkspaceService;
let appShell: IApplicationShell;

setup(async () => {
cmdManager = mock(CommandManager);
workspaceService = mock(WorkspaceService);
appShell = mock(ApplicationShell);

createNewFileCommandHandler = new CreatePythonFileCommandHandler(
instance(cmdManager),
instance(workspaceService),
instance(appShell),
);
when(workspaceService.getConfiguration('python')).thenReturn(
new MockWorkspaceConfiguration({
createNewFileEnabled: true,
}),
);
when(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).thenReturn(
Promise.resolve(({} as unknown) as TextDocument),
);
await createNewFileCommandHandler.activate();
});

test('Create Python file command is registered', async () => {
verify(cmdManager.registerCommand(Commands.CreateNewFile, anything(), anything())).once();
});
test('Create a Python file if command is executed', async () => {
await createNewFileCommandHandler.createPythonFile();
verify(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).once();
verify(appShell.showTextDocument(anything())).once();
});
});
luabud marked this conversation as resolved.
Show resolved Hide resolved