Skip to content

Commit

Permalink
Provide "New File" default implementation (#13303)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhuebner committed Mar 1, 2024
1 parent 8b00354 commit a35b9fc
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 40 deletions.
40 changes: 39 additions & 1 deletion packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ export namespace CommonCommands {
category: VIEW_CATEGORY,
label: 'Toggle Menu Bar'
});
export const NEW_FILE = Command.toDefaultLocalizedCommand({
id: 'workbench.action.files.newFile',
category: FILE_CATEGORY
});
export const NEW_UNTITLED_TEXT_FILE = Command.toDefaultLocalizedCommand({
id: 'workbench.action.files.newUntitledTextFile',
category: FILE_CATEGORY,
Expand Down Expand Up @@ -1424,6 +1428,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
const items: QuickPickItemOrSeparator[] = [
{
label: nls.localizeByDefault('New Text File'),
description: nls.localizeByDefault('Built-in'),
execute: async () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_TEXT_FILE.id)
},
...newFileContributions.children
Expand All @@ -1446,10 +1451,43 @@ export class CommonFrontendContribution implements FrontendApplicationContributi

})
];

const CREATE_NEW_FILE_ITEM_ID = 'create-new-file';
const hasNewFileHandler = this.commandRegistry.getActiveHandler(CommonCommands.NEW_FILE.id) !== undefined;
// Create a "Create New File" item only if there is a NEW_FILE command handler.
const createNewFileItem: QuickPickItem & { value?: string } | undefined = hasNewFileHandler ? {
id: CREATE_NEW_FILE_ITEM_ID,
label: nls.localizeByDefault('Create New File ({0})'),
description: nls.localizeByDefault('Built-in'),
execute: async () => {
if (createNewFileItem?.value) {
const parent = await this.workingDirProvider.getUserWorkingDir();
// Exec NEW_FILE command with the file name as additional argument
return this.commandRegistry.executeCommand(CommonCommands.NEW_FILE.id, parent, createNewFileItem.value);
}
}
} : undefined;

this.quickInputService.showQuickPick(items, {
title: nls.localizeByDefault('New File...'),
placeholder: nls.localizeByDefault('Select File Type or Enter File Name...'),
canSelectMany: false
canSelectMany: false,
onDidChangeValue: picker => {
if (createNewFileItem === undefined) {
return;
}
// Dynamically show or hide the "Create New File" item based on the input value.
if (picker.value) {
createNewFileItem.alwaysShow = true;
createNewFileItem.value = picker.value;
createNewFileItem.label = nls.localizeByDefault('Create New File ({0})', picker.value);
picker.items = [...items, createNewFileItem];
} else {
createNewFileItem.alwaysShow = false;
createNewFileItem.value = undefined;
picker.items = items.filter(item => item !== createNewFileItem);
}
}
});
}

Expand Down
71 changes: 57 additions & 14 deletions packages/filesystem/src/browser/filesystem-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,35 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { MaybePromise, SelectionService, isCancelled, Emitter } from '@theia/core/lib/common';
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { nls } from '@theia/core';
import {
FrontendApplicationContribution, ApplicationShell,
NavigatableWidget, NavigatableWidgetOptions,
Saveable, WidgetManager, StatefulWidget, FrontendApplication, ExpandableTreeNode,
CorePreferences,
ApplicationShell,
CommonCommands,
CorePreferences,
ExpandableTreeNode,
FrontendApplication,
FrontendApplicationContribution,
NavigatableWidget, NavigatableWidgetOptions,
OpenerService,
Saveable,
StatefulWidget,
WidgetManager,
open
} from '@theia/core/lib/browser';
import { MimeService } from '@theia/core/lib/browser/mime-service';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { FileSystemPreferences } from './filesystem-preferences';
import { Emitter, MaybePromise, SelectionService, isCancelled } from '@theia/core/lib/common';
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { Deferred } from '@theia/core/lib/common/promise-util';
import URI from '@theia/core/lib/common/uri';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FileChangeType, FileChangesEvent, FileOperation, FileStat } from '../common/files';
import { FileDialogService, SaveFileDialogProps } from './file-dialog';
import { FileSelection } from './file-selection';
import { FileUploadService, FileUploadResult } from './file-upload-service';
import { FileService, UserFileOperationEvent } from './file-service';
import { FileChangesEvent, FileChangeType, FileOperation } from '../common/files';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { nls } from '@theia/core';
import { FileUploadResult, FileUploadService } from './file-upload-service';
import { FileSystemPreferences } from './filesystem-preferences';

export namespace FileSystemCommands {

Expand Down Expand Up @@ -78,6 +86,12 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
@inject(FileService)
protected readonly fileService: FileService;

@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;

@inject(OpenerService)
protected readonly openerService: OpenerService;

protected onDidChangeEditorFileEmitter = new Emitter<{ editor: NavigatableWidget, type: FileChangeType }>();
readonly onDidChangeEditorFile = this.onDidChangeEditorFileEmitter.event;

Expand Down Expand Up @@ -134,6 +148,22 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
}
}
});
commands.registerCommand(CommonCommands.NEW_FILE, {
execute: async (...args: unknown[]) => {
let newFileName = undefined;
if (args !== undefined && typeof args[1] === 'string') {
newFileName = args[1];
}
const title = nls.localizeByDefault('Create File');
const props: SaveFileDialogProps = { title, saveLabel: title, inputValue: newFileName };
const selectedFile = this.getSelection(...args)?.fileStat ?? await this.getFolderFromArgs(...args);
const filePath = await this.fileDialogService.showSaveDialog(props, selectedFile);
if (filePath) {
const file = await this.fileService.createFile(filePath);
open(this.openerService, file.resource);
}
}
});
}

protected canUpload({ fileStat }: FileSelection): boolean {
Expand All @@ -160,6 +190,19 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
return this.toSelection(args[0]) ?? (Array.isArray(selection) ? selection.find(FileSelection.is) : this.toSelection(selection));
};

/**
* Retrieves the folder from the arguments.
* @param args The arguments passed to a command.
* @returns A promise that resolves to a FileStat representing the folder, or undefined if the folder cannot be found.
*/
protected async getFolderFromArgs(...args: unknown[]): Promise<FileStat | undefined> {
if (args[0] instanceof URI) {
const fileStat = await this.fileService.resolve(args[0] as URI);
return fileStat.isDirectory ? fileStat : undefined;
}
return undefined;
};

protected toSelection(arg: unknown): FileSelection | undefined {
return FileSelection.is(arg) ? arg : undefined;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/getting-started/src/browser/getting-started-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ export class GettingStartedWidget extends ReactWidget {
protected renderOpen(): React.ReactNode {
const requireSingleOpen = isOSX || !environment.electron.is();

const createFile = <div className='gs-action-container'>
<a
role={'button'}
tabIndex={0}
onClick={this.doCreateFile}
onKeyDown={this.doCreateFileEnter}>
{CommonCommands.NEW_UNTITLED_FILE.label ?? nls.localizeByDefault('New File...')}
</a>
</div>;

const open = requireSingleOpen && <div className='gs-action-container'>
<a
role={'button'}
Expand Down Expand Up @@ -224,6 +234,7 @@ export class GettingStartedWidget extends ReactWidget {

return <div className='gs-section'>
<h3 className='gs-section-header'><i className={codicon('folder-opened')}></i>{nls.localizeByDefault('Open')}</h3>
{createFile}
{open}
{openFile}
{openFolder}
Expand Down Expand Up @@ -392,6 +403,16 @@ export class GettingStartedWidget extends ReactWidget {
return paths;
}

/**
* Trigger the create file command.
*/
protected doCreateFile = () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_FILE.id);
protected doCreateFileEnter = (e: React.KeyboardEvent) => {
if (this.isEnterKey(e)) {
this.doCreateFile();
}
};

/**
* Trigger the open command.
*/
Expand Down
54 changes: 29 additions & 25 deletions packages/workspace/src/browser/workspace-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ export namespace WorkspaceCommands {
category: WORKSPACE_CATEGORY,
label: 'Close Workspace'
});

export const NEW_FILE = Command.toDefaultLocalizedCommand({
id: 'file.newFile',
category: FILE_CATEGORY,
label: 'New File...'
});

export const NEW_FOLDER = Command.toDefaultLocalizedCommand({
id: 'file.newFolder',
category: FILE_CATEGORY,
Expand Down Expand Up @@ -226,32 +228,34 @@ export class WorkspaceCommandContribution implements CommandContribution {
registerCommands(registry: CommandRegistry): void {
this.registerOpenWith(registry);
registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({
execute: uri => this.getDirectory(uri).then(parent => {
if (parent) {
const parentUri = parent.resource;
const { fileName, fileExtension } = this.getDefaultFileConfig();
const targetUri = parentUri.resolve(fileName + fileExtension);
const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parent, targetUri, false);

const dialog = new WorkspaceInputDialog({
title: nls.localizeByDefault('New File...'),
maxWidth: 400,
parentUri: parentUri,
initialValue: vacantChildUri.path.base,
placeholder: nls.localize('theia/workspace/newFilePlaceholder', 'File Name'),
validate: name => this.validateFileName(name, parent, true)
}, this.labelProvider);

dialog.open().then(async name => {
if (name) {
const fileUri = parentUri.resolve(name);
await this.fileService.create(fileUri);
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
open(this.openerService, fileUri);
}
});
execute: async uri => {
const parent = await this.getDirectory(uri);
if (parent === undefined) {
return;
}
})
const parentUri = parent.resource;
const { fileName, fileExtension } = this.getDefaultFileConfig();
const targetUri = parentUri.resolve(fileName + fileExtension);
const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parent, targetUri, false);

// Open dialog
const dialog = new WorkspaceInputDialog({
title: nls.localizeByDefault('New File...'),
maxWidth: 400,
parentUri: parentUri,
initialValue: vacantChildUri.path.base,
placeholder: nls.localize('theia/workspace/newFilePlaceholder', 'File Name'),
validate: name => this.validateFileName(name, parent, true)
}, this.labelProvider);

const newFileName = await dialog.open();
if (newFileName !== undefined) {
const fileUri = parentUri.resolve(newFileName);
await this.fileService.create(fileUri);
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
open(this.openerService, fileUri);
}
}
}));
registry.registerCommand(WorkspaceCommands.NEW_FOLDER, this.newWorkspaceRootUriAwareCommandHandler({
execute: uri => this.getDirectory(uri).then(parent => {
Expand Down

0 comments on commit a35b9fc

Please sign in to comment.