Path: blob/main/src/vs/workbench/browser/actions/workspaceCommands.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize, localize2 } from '../../../nls.js';6import { hasWorkspaceFileExtension, IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js';7import { IWorkspaceEditingService } from '../../services/workspaces/common/workspaceEditing.js';8import { dirname } from '../../../base/common/resources.js';9import { CancellationToken } from '../../../base/common/cancellation.js';10import { mnemonicButtonLabel } from '../../../base/common/labels.js';11import { CommandsRegistry, ICommandService } from '../../../platform/commands/common/commands.js';12import { FileKind } from '../../../platform/files/common/files.js';13import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';14import { ILabelService } from '../../../platform/label/common/label.js';15import { IQuickInputService, IPickOptions, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';16import { getIconClasses } from '../../../editor/common/services/getIconClasses.js';17import { IModelService } from '../../../editor/common/services/model.js';18import { ILanguageService } from '../../../editor/common/languages/language.js';19import { IFileDialogService, IPickAndOpenOptions } from '../../../platform/dialogs/common/dialogs.js';20import { URI, UriComponents } from '../../../base/common/uri.js';21import { Schemas } from '../../../base/common/network.js';22import { IFileToOpen, IFolderToOpen, IOpenEmptyWindowOptions, IOpenWindowOptions, IWorkspaceToOpen } from '../../../platform/window/common/window.js';23import { IRecent, IWorkspacesService } from '../../../platform/workspaces/common/workspaces.js';24import { IPathService } from '../../services/path/common/pathService.js';25import { ILocalizedString } from '../../../platform/action/common/action.js';2627export const ADD_ROOT_FOLDER_COMMAND_ID = 'addRootFolder';28export const ADD_ROOT_FOLDER_LABEL: ILocalizedString = localize2('addFolderToWorkspace', 'Add Folder to Workspace...');2930export const SET_ROOT_FOLDER_COMMAND_ID = 'setRootFolder';3132export const PICK_WORKSPACE_FOLDER_COMMAND_ID = '_workbench.pickWorkspaceFolder';3334// Command registration3536CommandsRegistry.registerCommand({37id: 'workbench.action.files.openFileFolderInNewWindow',38handler: (accessor: ServicesAccessor) => accessor.get(IFileDialogService).pickFileFolderAndOpen({ forceNewWindow: true })39});4041CommandsRegistry.registerCommand({42id: '_files.pickFolderAndOpen',43handler: (accessor: ServicesAccessor, options: { forceNewWindow: boolean }) => accessor.get(IFileDialogService).pickFolderAndOpen(options)44});4546CommandsRegistry.registerCommand({47id: 'workbench.action.files.openFolderInNewWindow',48handler: (accessor: ServicesAccessor) => accessor.get(IFileDialogService).pickFolderAndOpen({ forceNewWindow: true })49});5051CommandsRegistry.registerCommand({52id: 'workbench.action.files.openFileInNewWindow',53handler: (accessor: ServicesAccessor) => accessor.get(IFileDialogService).pickFileAndOpen({ forceNewWindow: true })54});5556CommandsRegistry.registerCommand({57id: 'workbench.action.openWorkspaceInNewWindow',58handler: (accessor: ServicesAccessor) => accessor.get(IFileDialogService).pickWorkspaceAndOpen({ forceNewWindow: true })59});6061CommandsRegistry.registerCommand({62id: ADD_ROOT_FOLDER_COMMAND_ID,63handler: async (accessor) => {64const workspaceEditingService = accessor.get(IWorkspaceEditingService);6566const folders = await selectWorkspaceFolders(accessor);67if (!folders || !folders.length) {68return;69}7071await workspaceEditingService.addFolders(folders.map(folder => ({ uri: folder })));72}73});7475CommandsRegistry.registerCommand({76id: SET_ROOT_FOLDER_COMMAND_ID,77handler: async (accessor) => {78const workspaceEditingService = accessor.get(IWorkspaceEditingService);79const contextService = accessor.get(IWorkspaceContextService);8081const folders = await selectWorkspaceFolders(accessor);82if (!folders || !folders.length) {83return;84}8586await workspaceEditingService.updateFolders(0, contextService.getWorkspace().folders.length, folders.map(folder => ({ uri: folder })));87}88});8990async function selectWorkspaceFolders(accessor: ServicesAccessor): Promise<URI[] | undefined> {91const dialogsService = accessor.get(IFileDialogService);92const pathService = accessor.get(IPathService);9394const folders = await dialogsService.showOpenDialog({95openLabel: mnemonicButtonLabel(localize({ key: 'add', comment: ['&& denotes a mnemonic'] }, "&&Add")),96title: localize('addFolderToWorkspaceTitle', "Add Folder to Workspace"),97canSelectFolders: true,98canSelectMany: true,99defaultUri: await dialogsService.defaultFolderPath(),100availableFileSystems: [pathService.defaultUriScheme]101});102103return folders;104}105106CommandsRegistry.registerCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, async function (accessor, args?: [IPickOptions<IQuickPickItem>, CancellationToken]) {107const quickInputService = accessor.get(IQuickInputService);108const labelService = accessor.get(ILabelService);109const contextService = accessor.get(IWorkspaceContextService);110const modelService = accessor.get(IModelService);111const languageService = accessor.get(ILanguageService);112113const folders = contextService.getWorkspace().folders;114if (!folders.length) {115return;116}117118const folderPicks: IQuickPickItem[] = folders.map(folder => {119const label = folder.name;120const description = labelService.getUriLabel(dirname(folder.uri), { relative: true });121122return {123label,124description: description !== label ? description : undefined, // https://github.com/microsoft/vscode/issues/183418125folder,126iconClasses: getIconClasses(modelService, languageService, folder.uri, FileKind.ROOT_FOLDER)127};128});129130const options: IPickOptions<IQuickPickItem> = (args ? args[0] : undefined) || Object.create(null);131132if (!options.activeItem) {133options.activeItem = folderPicks[0];134}135136if (!options.placeHolder) {137options.placeHolder = localize('workspaceFolderPickerPlaceholder', "Select workspace folder");138}139140if (typeof options.matchOnDescription !== 'boolean') {141options.matchOnDescription = true;142}143144const token: CancellationToken = (args ? args[1] : undefined) || CancellationToken.None;145const pick = await quickInputService.pick(folderPicks, options, token);146if (pick) {147return folders[folderPicks.indexOf(pick)];148}149150return;151});152153// API Command registration154155interface IOpenFolderAPICommandOptions {156forceNewWindow?: boolean;157forceReuseWindow?: boolean;158noRecentEntry?: boolean;159forceLocalWindow?: boolean;160forceProfile?: string;161forceTempProfile?: boolean;162filesToOpen?: UriComponents[];163}164165CommandsRegistry.registerCommand({166id: 'vscode.openFolder',167handler: (accessor: ServicesAccessor, uriComponents?: UriComponents, arg?: boolean | IOpenFolderAPICommandOptions) => {168const commandService = accessor.get(ICommandService);169170// Be compatible to previous args by converting to options171if (typeof arg === 'boolean') {172arg = { forceNewWindow: arg };173}174175// Without URI, ask to pick a folder or workspace to open176if (!uriComponents) {177const options: IPickAndOpenOptions = {178forceNewWindow: arg?.forceNewWindow179};180181if (arg?.forceLocalWindow) {182options.remoteAuthority = null;183options.availableFileSystems = ['file'];184}185186return commandService.executeCommand('_files.pickFolderAndOpen', options);187}188189const uri = URI.from(uriComponents, true);190191const options: IOpenWindowOptions = {192forceNewWindow: arg?.forceNewWindow,193forceReuseWindow: arg?.forceReuseWindow,194noRecentEntry: arg?.noRecentEntry,195remoteAuthority: arg?.forceLocalWindow ? null : undefined,196forceProfile: arg?.forceProfile,197forceTempProfile: arg?.forceTempProfile,198};199200const workspaceToOpen: IWorkspaceToOpen | IFolderToOpen = (hasWorkspaceFileExtension(uri) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri };201const filesToOpen: IFileToOpen[] = arg?.filesToOpen?.map(file => ({ fileUri: URI.from(file, true) })) ?? [];202return commandService.executeCommand('_files.windowOpen', [workspaceToOpen, ...filesToOpen], options);203},204metadata: {205description: 'Open a folder or workspace in the current window or new window depending on the newWindow argument. Note that opening in the same window will shutdown the current extension host process and start a new one on the given folder/workspace unless the newWindow parameter is set to true.',206args: [207{208name: 'uri', description: '(optional) Uri of the folder or workspace file to open. If not provided, a native dialog will ask the user for the folder',209constraint: (value: any) => value === undefined || value === null || value instanceof URI210},211{212name: 'options',213description: '(optional) Options. Object with the following properties: ' +214'`forceNewWindow`: Whether to open the folder/workspace in a new window or the same. Defaults to opening in the same window. ' +215'`forceReuseWindow`: Whether to force opening the folder/workspace in the same window. Defaults to false. ' +216'`noRecentEntry`: Whether the opened URI will appear in the \'Open Recent\' list. Defaults to false. ' +217'`forceLocalWindow`: Whether to force opening the folder/workspace in a local window. Defaults to false. ' +218'`forceProfile`: The profile to use when opening the folder/workspace. Defaults to the current profile. ' +219'`forceTempProfile`: Whether to use a temporary profile when opening the folder/workspace. Defaults to false. ' +220'`filesToOpen`: An array of files to open in the new window. Defaults to an empty array. ' +221'Note, for backward compatibility, options can also be of type boolean, representing the `forceNewWindow` setting.',222constraint: (value: any) => value === undefined || typeof value === 'object' || typeof value === 'boolean'223}224]225}226});227228interface INewWindowAPICommandOptions {229reuseWindow?: boolean;230/**231* If set, defines the remoteAuthority of the new window. `null` will open a local window.232* If not set, defaults to remoteAuthority of the current window.233*/234remoteAuthority?: string | null;235}236237CommandsRegistry.registerCommand({238id: 'vscode.newWindow',239handler: (accessor: ServicesAccessor, options?: INewWindowAPICommandOptions) => {240const commandService = accessor.get(ICommandService);241242const commandOptions: IOpenEmptyWindowOptions = {243forceReuseWindow: options && options.reuseWindow,244remoteAuthority: options && options.remoteAuthority245};246247return commandService.executeCommand('_files.newWindow', commandOptions);248},249metadata: {250description: 'Opens an new window depending on the newWindow argument.',251args: [252{253name: 'options',254description: '(optional) Options. Object with the following properties: ' +255'`reuseWindow`: Whether to open a new window or the same. Defaults to opening in a new window. ',256constraint: (value: any) => value === undefined || typeof value === 'object'257}258]259}260});261262// recent history commands263264CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) {265const workspacesService = accessor.get(IWorkspacesService);266return workspacesService.removeRecentlyOpened([uri]);267});268269CommandsRegistry.registerCommand({270id: 'vscode.removeFromRecentlyOpened',271handler: (accessor: ServicesAccessor, path: string | URI): Promise<void> => {272const workspacesService = accessor.get(IWorkspacesService);273274if (typeof path === 'string') {275path = path.match(/^[^:/?#]+:\/\//) ? URI.parse(path) : URI.file(path);276} else {277path = URI.revive(path); // called from extension host278}279280return workspacesService.removeRecentlyOpened([path]);281},282metadata: {283description: 'Removes an entry with the given path from the recently opened list.',284args: [285{ name: 'path', description: 'URI or URI string to remove from recently opened.', constraint: (value: any) => typeof value === 'string' || value instanceof URI }286]287}288});289290interface RecentEntry {291uri: URI;292type: 'workspace' | 'folder' | 'file';293label?: string;294remoteAuthority?: string;295}296297CommandsRegistry.registerCommand('_workbench.addToRecentlyOpened', async function (accessor: ServicesAccessor, recentEntry: RecentEntry) {298const workspacesService = accessor.get(IWorkspacesService);299const uri = recentEntry.uri;300const label = recentEntry.label;301const remoteAuthority = recentEntry.remoteAuthority;302303let recent: IRecent | undefined = undefined;304if (recentEntry.type === 'workspace') {305const workspace = await workspacesService.getWorkspaceIdentifier(uri);306recent = { workspace, label, remoteAuthority };307} else if (recentEntry.type === 'folder') {308recent = { folderUri: uri, label, remoteAuthority };309} else {310recent = { fileUri: uri, label, remoteAuthority };311}312313return workspacesService.addRecentlyOpened([recent]);314});315316CommandsRegistry.registerCommand('_workbench.getRecentlyOpened', async function (accessor: ServicesAccessor) {317const workspacesService = accessor.get(IWorkspacesService);318319return workspacesService.getRecentlyOpened();320});321322323