Skip to content

Commit 273cf57

Browse files
luabudKartik Rajkimadeline
authored
Create new Python file command (#18522)
* Create new Python file command * Rename command * Format documents * Add a news entry * Add telemetry and add experimental setting * Add tests * Format document * Disable eslint class-methods-use-this * Attempt to fix test * Add workspace service * Try to fix test again * Fix tests and add appShell * Add title for command * Add titles to package.nls.json * Remove accidental launch.json edit * Apply suggestions from code review Co-authored-by: Kartik Raj <[email protected]> * Add shorttitle to new file command * Apply suggestions from code review Co-authored-by: Kim-Adeline Miguel <[email protected]> Co-authored-by: Kartik Raj <[email protected]> Co-authored-by: Kim-Adeline Miguel <[email protected]>
1 parent ff2a13b commit 273cf57

File tree

13 files changed

+160
-0
lines changed

13 files changed

+160
-0
lines changed

news/1 Enhancements/18376.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement a "New Python File" command

package.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"onDebugInitialConfigurations",
6666
"onLanguage:python",
6767
"onDebugResolve:python",
68+
"onCommand:python.createNewFile",
6869
"onCommand:python.execInTerminal",
6970
"onCommand:python.debugInTerminal",
7071
"onCommand:python.sortImports",
@@ -322,6 +323,13 @@
322323
}
323324
],
324325
"commands": [
326+
{
327+
"title": "%python.command.python.createNewFile.title%",
328+
"shortTitle": "%python.menu.createNewFile.title%",
329+
"category": "Python",
330+
"command": "python.createNewFile",
331+
"when": "config.python.createNewFileEnabled"
332+
},
325333
{
326334
"category": "Python",
327335
"command": "python.analysis.restartLanguageServer",
@@ -503,6 +511,15 @@
503511
"scope": "machine",
504512
"type": "string"
505513
},
514+
"python.createNewFileEnabled": {
515+
"default": "false",
516+
"description": "Enable the `Python: New Python File` command.",
517+
"scope": "machine",
518+
"type": "boolean",
519+
"tags": [
520+
"experimental"
521+
]
522+
},
506523
"python.defaultInterpreterPath": {
507524
"default": "python",
508525
"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.",
@@ -1862,6 +1879,13 @@
18621879
"when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported"
18631880
}
18641881
],
1882+
"file/newFile": [
1883+
{
1884+
"command": "python.createNewFile",
1885+
"category": "file",
1886+
"when": "config.python.createNewFileEnabled"
1887+
}
1888+
],
18651889
"view/title": [
18661890
{
18671891
"command": "python.refreshTests",

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"python.command.python.sortImports.title": "Sort Imports",
33
"python.command.python.startREPL.title": "Start REPL",
44
"python.command.python.createTerminal.title": "Create Terminal",
5+
"python.command.python.createNewFile.title": "New Python File",
56
"python.command.python.execInTerminal.title": "Run Python File in Terminal",
67
"python.command.python.debugInTerminal.title": "Debug Python File",
78
"python.command.python.execInTerminalIcon.title": "Run Python File",
@@ -29,6 +30,7 @@
2930
"python.command.python.analysis.restartLanguageServer.title": "Restart Language Server",
3031
"python.command.python.launchTensorBoard.title": "Launch TensorBoard",
3132
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
33+
"python.menu.createNewFile.title": "Python File",
3234
"python.snippet.launch.standard.label": "Python: Current File",
3335
"python.snippet.launch.module.label": "Python: Module",
3436
"python.snippet.launch.module.default": "enter-your-module-name",

src/client/common/application/applicationShell.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ import {
2626
SaveDialogOptions,
2727
StatusBarAlignment,
2828
StatusBarItem,
29+
TextDocument,
30+
TextEditor,
2931
TreeView,
3032
TreeViewOptions,
3133
Uri,
34+
ViewColumn,
3235
window,
3336
WindowState,
3437
WorkspaceFolder,
@@ -100,6 +103,14 @@ export class ApplicationShell implements IApplicationShell {
100103
public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined> {
101104
return window.showInputBox(options, token);
102105
}
106+
public showTextDocument(
107+
document: TextDocument,
108+
column?: ViewColumn,
109+
preserveFocus?: boolean,
110+
): Thenable<TextEditor> {
111+
return window.showTextDocument(document, column, preserveFocus);
112+
}
113+
103114
public openUrl(url: string): void {
104115
env.openExternal(Uri.parse(url));
105116
}

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
4242
[Commands.PickLocalProcess]: [];
4343
[Commands.ClearStorage]: [];
4444
[Commands.ReportIssue]: [];
45+
[Commands.CreateNewFile]: [];
4546
[Commands.RefreshTensorBoard]: [];
4647
[LSCommands.RestartLS]: [];
4748
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { injectable, inject } from 'inversify';
2+
import { IExtensionSingleActivationService } from '../../../activation/types';
3+
import { Commands } from '../../constants';
4+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../types';
5+
import { sendTelemetryEvent } from '../../../telemetry';
6+
import { EventName } from '../../../telemetry/constants';
7+
8+
@injectable()
9+
export class CreatePythonFileCommandHandler implements IExtensionSingleActivationService {
10+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true };
11+
12+
constructor(
13+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
14+
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
15+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
16+
) {}
17+
18+
public async activate(): Promise<void> {
19+
if (!this.workspaceService.getConfiguration('python').get<boolean>('createNewFileEnabled')) {
20+
return;
21+
}
22+
this.commandManager.registerCommand(Commands.CreateNewFile, this.createPythonFile, this);
23+
}
24+
25+
public async createPythonFile(): Promise<void> {
26+
const newFile = await this.workspaceService.openTextDocument({ language: 'python' });
27+
this.appShell.showTextDocument(newFile);
28+
sendTelemetryEvent(EventName.CREATE_NEW_FILE_COMMAND);
29+
}
30+
}

src/client/common/application/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,19 @@ export interface IApplicationShell {
273273
*/
274274
showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined>;
275275

276+
/**
277+
* Show the given document in a text editor. A {@link ViewColumn column} can be provided
278+
* to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}.
279+
*
280+
* @param document A text document to be shown.
281+
* @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values
282+
* are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@linkcode ViewColumn.Beside}
283+
* to open the editor to the side of the currently active one.
284+
* @param preserveFocus When `true` the editor will not take focus.
285+
* @return A promise that resolves to an {@link TextEditor editor}.
286+
*/
287+
showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable<TextEditor>;
288+
276289
/**
277290
* Creates a [QuickPick](#QuickPick) to let the user pick an item from a list
278291
* of items of type T.
@@ -833,6 +846,16 @@ export interface IWorkspaceService {
833846
* @return The full configuration or a subset.
834847
*/
835848
getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration;
849+
850+
/**
851+
* Opens an untitled text document. The editor will prompt the user for a file
852+
* path when the document is to be saved. The `options` parameter allows to
853+
* specify the *language* and/or the *content* of the document.
854+
*
855+
* @param options Options to control how the document will be created.
856+
* @return A promise that resolves to a {@link TextDocument document}.
857+
*/
858+
openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument>;
836859
}
837860

838861
export const ITerminalManager = Symbol('ITerminalManager');

src/client/common/application/workspace.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Event,
1010
FileSystemWatcher,
1111
GlobPattern,
12+
TextDocument,
1213
Uri,
1314
workspace,
1415
WorkspaceConfiguration,
@@ -97,6 +98,10 @@ export class WorkspaceService implements IWorkspaceService {
9798
return workspace.onDidGrantWorkspaceTrust;
9899
}
99100

101+
public openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument> {
102+
return workspace.openTextDocument(options);
103+
}
104+
100105
private get searchExcludes() {
101106
const searchExcludes = this.getConfiguration('search.exclude');
102107
const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true);

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export namespace Commands {
4949
export const ViewOutput = 'python.viewOutput';
5050
export const Start_REPL = 'python.startREPL';
5151
export const Create_Terminal = 'python.createTerminal';
52+
export const CreateNewFile = 'python.createNewFile';
5253
export const Set_Linter = 'python.setLinter';
5354
export const Enable_Linter = 'python.enableLinting';
5455
export const Run_Linter = 'python.runLinting';

src/client/common/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { ClipboardService } from './application/clipboard';
3131
import { CommandManager } from './application/commandManager';
3232
import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand';
3333
import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand';
34+
import { CreatePythonFileCommandHandler } from './application/commands/createFileCommand';
3435
import { DebugService } from './application/debugService';
3536
import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
3637
import { DocumentManager } from './application/documentManager';
@@ -198,6 +199,10 @@ export function registerTypes(serviceManager: IServiceManager): void {
198199
IExtensionSingleActivationService,
199200
ReportIssueCommandHandler,
200201
);
202+
serviceManager.addSingleton<IExtensionSingleActivationService>(
203+
IExtensionSingleActivationService,
204+
CreatePythonFileCommandHandler,
205+
);
201206
serviceManager.addSingleton<IExtensionChannelService>(IExtensionChannelService, ExtensionChannelService);
202207
serviceManager.addSingleton<IExtensionChannelRule>(
203208
IExtensionChannelRule,

src/client/telemetry/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export enum EventName {
9494

9595
SELECT_LINTER = 'LINTING.SELECT',
9696
USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND',
97+
CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND',
9798

9899
LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT',
99100
HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME',

src/client/telemetry/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,13 @@ export interface IEventNamePropertyMapping {
15921592
"use_report_issue_command" : { }
15931593
*/
15941594
[EventName.USE_REPORT_ISSUE_COMMAND]: unknown;
1595+
/**
1596+
* Telemetry event sent when the New Python File command is executed.
1597+
*/
1598+
/* __GDPR__
1599+
"create_new_file_command" : { }
1600+
*/
1601+
[EventName.CREATE_NEW_FILE_COMMAND]: unknown;
15951602
/**
15961603
* Telemetry event sent once on session start with details on which experiments are opted into and opted out from.
15971604
*/
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
5+
import { TextDocument } from 'vscode';
6+
import { Commands } from '../../../../client/common/constants';
7+
import { CommandManager } from '../../../../client/common/application/commandManager';
8+
import { CreatePythonFileCommandHandler } from '../../../../client/common/application/commands/createFileCommand';
9+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types';
10+
import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig';
11+
import { WorkspaceService } from '../../../../client/common/application/workspace';
12+
import { ApplicationShell } from '../../../../client/common/application/applicationShell';
13+
14+
suite('Create New Python File Commmand', () => {
15+
let createNewFileCommandHandler: CreatePythonFileCommandHandler;
16+
let cmdManager: ICommandManager;
17+
let workspaceService: IWorkspaceService;
18+
let appShell: IApplicationShell;
19+
20+
setup(async () => {
21+
cmdManager = mock(CommandManager);
22+
workspaceService = mock(WorkspaceService);
23+
appShell = mock(ApplicationShell);
24+
25+
createNewFileCommandHandler = new CreatePythonFileCommandHandler(
26+
instance(cmdManager),
27+
instance(workspaceService),
28+
instance(appShell),
29+
);
30+
when(workspaceService.getConfiguration('python')).thenReturn(
31+
new MockWorkspaceConfiguration({
32+
createNewFileEnabled: true,
33+
}),
34+
);
35+
when(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).thenReturn(
36+
Promise.resolve(({} as unknown) as TextDocument),
37+
);
38+
await createNewFileCommandHandler.activate();
39+
});
40+
41+
test('Create Python file command is registered', async () => {
42+
verify(cmdManager.registerCommand(Commands.CreateNewFile, anything(), anything())).once();
43+
});
44+
test('Create a Python file if command is executed', async () => {
45+
await createNewFileCommandHandler.createPythonFile();
46+
verify(workspaceService.openTextDocument(deepEqual({ language: 'python' }))).once();
47+
verify(appShell.showTextDocument(anything())).once();
48+
});
49+
});

0 commit comments

Comments
 (0)