diff --git a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts index 50cab54fc3442..d106431ea9422 100644 --- a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts +++ b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts @@ -24,7 +24,7 @@ import { ILogger } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { PluginPaths } from './const'; import { PluginPathsService } from '../../common/plugin-paths-protocol'; -import { THEIA_EXT, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '@theia/workspace/lib/common'; +import { CommonWorkspaceUtils } from '@theia/workspace/lib/common'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginCliContribution } from '../plugin-cli-contribution'; @@ -43,6 +43,9 @@ export class PluginPathsServiceImpl implements PluginPathsService { @inject(PluginCliContribution) protected readonly cliContribution: PluginCliContribution; + @inject(CommonWorkspaceUtils) + protected readonly workspaceUtils: CommonWorkspaceUtils; + async getHostLogPath(): Promise { const parentLogsDir = await this.getLogsDirPath(); @@ -78,7 +81,11 @@ export class PluginPathsServiceImpl implements PluginPathsService { } protected async buildWorkspaceId(workspaceUri: string, rootUris: string[]): Promise { - const untitledWorkspace = await getTemporaryWorkspaceFileUri(this.envServer); + const configDirUri = await this.envServer.getConfigDirUri(); + const untitledWorkspace = await this.workspaceUtils.getUntitledWorkspaceUri( + new URI(configDirUri), + async uri => !await fs.pathExists(uri.path.fsPath()) + ); if (untitledWorkspace.toString() === workspaceUri) { // if workspace is temporary @@ -86,15 +93,6 @@ export class PluginPathsServiceImpl implements PluginPathsService { const rootsStr = rootUris.sort().join(','); return this.createHash(rootsStr); } else { - let stat; - try { - stat = await fs.stat(FileUri.fsPath(workspaceUri)); - } catch { /* no-op */ } - let displayName = new URI(workspaceUri).displayName; - if ((!stat || !stat.isDirectory()) && (displayName.endsWith(`.${THEIA_EXT}`) || displayName.endsWith(`.${VSCODE_EXT}`))) { - displayName = displayName.slice(0, displayName.lastIndexOf('.')); - } - return this.createHash(workspaceUri); } } diff --git a/packages/preferences/src/browser/preferences-monaco-contribution.ts b/packages/preferences/src/browser/preferences-monaco-contribution.ts index 950560fccfefb..b02facd5288b4 100644 --- a/packages/preferences/src/browser/preferences-monaco-contribution.ts +++ b/packages/preferences/src/browser/preferences-monaco-contribution.ts @@ -23,8 +23,5 @@ monaco.languages.register({ ], 'filenames': [ 'settings.json' - ], - 'extensions': [ - '.theia-workspace' ] }); diff --git a/packages/workspace/package.json b/packages/workspace/package.json index d2234a043c948..8114a00efab1b 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -5,6 +5,7 @@ "dependencies": { "@theia/core": "1.36.0", "@theia/filesystem": "1.36.0", + "@theia/monaco-editor-core": "1.72.3", "@theia/variable-resolver": "1.36.0", "jsonc-parser": "^2.2.0", "valid-filename": "^2.0.1" diff --git a/packages/workspace/src/browser/workspace-frontend-contribution.ts b/packages/workspace/src/browser/workspace-frontend-contribution.ts index 47de27b034240..926c9d511dc35 100644 --- a/packages/workspace/src/browser/workspace-frontend-contribution.ts +++ b/packages/workspace/src/browser/workspace-frontend-contribution.ts @@ -24,7 +24,7 @@ import { import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { WorkspaceService } from './workspace-service'; -import { THEIA_EXT, VSCODE_EXT } from '../common'; +import { CommonWorkspaceUtils, THEIA_EXT, VSCODE_EXT } from '../common'; import { WorkspaceCommands } from './workspace-commands'; import { QuickOpenWorkspace } from './quick-open-workspace'; import { WorkspacePreferences } from './workspace-preferences'; @@ -40,6 +40,8 @@ import { FileStat } from '@theia/filesystem/lib/common/files'; import { UntitledWorkspaceExitDialog } from './untitled-workspace-exit-dialog'; import { FilesystemSaveResourceService } from '@theia/filesystem/lib/browser/filesystem-save-resource-service'; import { StopReason } from '@theia/core/lib/common/frontend-application-state'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import * as monaco from '@theia/monaco-editor-core'; export enum WorkspaceStates { /** @@ -75,9 +77,19 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi @inject(EncodingRegistry) protected readonly encodingRegistry: EncodingRegistry; @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations; @inject(FilesystemSaveResourceService) protected readonly saveService: FilesystemSaveResourceService; + @inject(CommonWorkspaceUtils) protected readonly workspaceUtils: CommonWorkspaceUtils; configure(): void { - this.encodingRegistry.registerOverride({ encoding: UTF8, extension: THEIA_EXT }); + monaco.languages.register({ + id: 'jsonc', + 'aliases': [ + 'JSON with Comments' + ], + 'extensions': [ + `.${this.workspaceUtils.getWorkspaceExtension()}` + ] + }); + this.encodingRegistry.registerOverride({ encoding: UTF8, extension: this.workspaceUtils.getWorkspaceExtension() }); this.encodingRegistry.registerOverride({ encoding: UTF8, extension: VSCODE_EXT }); this.updateEncodingOverrides(); @@ -404,6 +416,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi return WorkspaceFrontendContribution.createOpenWorkspaceOpenFileDialogProps({ type, electron, + filters: this.getWorkspaceDialogFileFilters() }); } @@ -421,12 +434,13 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi do { selected = await this.fileDialogService.showSaveDialog({ title: WorkspaceCommands.SAVE_WORKSPACE_AS.label!, - filters: WorkspaceFrontendContribution.DEFAULT_FILE_FILTER + filters: this.getWorkspaceDialogFileFilters() }); if (selected) { const displayName = selected.displayName; - if (!displayName.endsWith(`.${THEIA_EXT}`) && !displayName.endsWith(`.${VSCODE_EXT}`)) { - selected = selected.parent.resolve(`${displayName}.${THEIA_EXT}`); + if (!displayName.endsWith(`.${this.workspaceUtils.getWorkspaceExtension()}`) + && !(this.workspaceUtils.isVSCodeWorkspaceSelectionEnabled() && displayName.endsWith(`.${VSCODE_EXT}`))) { + selected = selected.parent.resolve(`${displayName}.${this.workspaceUtils.getWorkspaceExtension()}`); } exist = await this.fileService.exists(selected); if (exist) { @@ -469,6 +483,17 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi return 'empty'; } + protected getWorkspaceDialogFileFilters(): FileDialogTreeFilters { + const workspaceExt = this.workspaceUtils.getWorkspaceExtension(); + const filters: FileDialogTreeFilters = { + [`${nls.localizeByDefault('{0} workspace', FrontendApplicationConfigProvider.get().applicationName)} (*.${workspaceExt})`]: [workspaceExt] + }; + if (this.workspaceUtils.isVSCodeWorkspaceSelectionEnabled()) { + filters[`${nls.localizeByDefault('{0} workspace', 'VS Code')} (*.${VSCODE_EXT})`] = [VSCODE_EXT]; + } + return filters; + } + private isElectron(): boolean { return environment.electron.is(); } @@ -514,6 +539,8 @@ export namespace WorkspaceFrontendContribution { /** * File filter for all Theia and VS Code workspace file types. + * + * @deprecated Since 1.37.0 Use `WorkspaceFrontendContribution#getWorkspaceDialogFileFilters` instead. */ export const DEFAULT_FILE_FILTER: FileDialogTreeFilters = { 'Theia Workspace (*.theia-workspace)': [THEIA_EXT], @@ -523,8 +550,8 @@ export namespace WorkspaceFrontendContribution { /** * Returns with an `OpenFileDialogProps` for opening the `Open Workspace` dialog. */ - export function createOpenWorkspaceOpenFileDialogProps(options: Readonly<{ type: OS.Type, electron: boolean }>): OpenFileDialogProps { - const { electron, type } = options; + export function createOpenWorkspaceOpenFileDialogProps(options: Readonly<{ type: OS.Type, electron: boolean, filters?: FileDialogTreeFilters }>): OpenFileDialogProps { + const { electron, type, filters = DEFAULT_FILE_FILTER } = options; const title = WorkspaceCommands.OPEN_WORKSPACE.dialogLabel; // If browser if (!electron) { @@ -532,7 +559,7 @@ export namespace WorkspaceFrontendContribution { title, canSelectFiles: true, canSelectFolders: true, - filters: DEFAULT_FILE_FILTER + filters }; } @@ -543,7 +570,7 @@ export namespace WorkspaceFrontendContribution { title, canSelectFiles: true, canSelectFolders: true, - filters: DEFAULT_FILE_FILTER + filters }; } @@ -551,7 +578,7 @@ export namespace WorkspaceFrontendContribution { title, canSelectFiles: true, canSelectFolders: false, - filters: DEFAULT_FILE_FILTER + filters }; } diff --git a/packages/workspace/src/browser/workspace-schema-updater.ts b/packages/workspace/src/browser/workspace-schema-updater.ts index 8f7fcc48f96d9..52da762ce81f9 100644 --- a/packages/workspace/src/browser/workspace-schema-updater.ts +++ b/packages/workspace/src/browser/workspace-schema-updater.ts @@ -20,6 +20,7 @@ import { InMemoryResources, isArray, isObject } from '@theia/core/lib/common'; import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import URI from '@theia/core/lib/common/uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { CommonWorkspaceUtils, VSCODE_EXT } from '../common'; export interface SchemaUpdateMessage { key: string, @@ -39,6 +40,7 @@ export class WorkspaceSchemaUpdater implements JsonSchemaContribution { protected safeToHandleQueue = new Deferred(); @inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources; + @inject(CommonWorkspaceUtils) protected readonly workspaceUtils: CommonWorkspaceUtils; @postConstruct() protected init(): void { @@ -47,8 +49,12 @@ export class WorkspaceSchemaUpdater implements JsonSchemaContribution { } registerSchemas(context: JsonSchemaRegisterContext): void { + const extensions = [this.workspaceUtils.getWorkspaceExtension()]; + if (this.workspaceUtils.isVSCodeWorkspaceSelectionEnabled()) { + extensions.push(VSCODE_EXT); + } context.registerSchema({ - fileMatch: ['*.theia-workspace', '*.code-workspace'], + fileMatch: extensions.map(ext => `*.${ext}`), url: this.uri.toString() }); } diff --git a/packages/workspace/src/common/utils.ts b/packages/workspace/src/common/utils.ts index b2c62a9b4d753..03a12b16d897a 100644 --- a/packages/workspace/src/common/utils.ts +++ b/packages/workspace/src/common/utils.ts @@ -17,22 +17,15 @@ // TODO get rid of util files, replace with methods in a responsible class import URI from '@theia/core/lib/common/uri'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { injectable } from '@theia/core/shared/inversify'; import { FileStat } from '@theia/filesystem/lib/common/files'; import { MaybePromise } from '@theia/core'; -export const THEIA_EXT = 'theia-workspace'; -export const VSCODE_EXT = 'code-workspace'; - /** - * @deprecated since 1.4.0 - because of https://github.com/eclipse-theia/theia/tree/master/doc/coding-guidelines.md#di-function-export, - * use `WorkspaceService.getUntitledWorkspace` instead + * @deprecated Since 1.37.0. Use `CommonWorkspaceUtils#getWorkspaceExtension` instead. */ -export async function getTemporaryWorkspaceFileUri(envVariableServer: EnvVariablesServer): Promise { - const configDirUri = await envVariableServer.getConfigDirUri(); - return new URI(configDirUri).resolve(`Untitled.${THEIA_EXT}`); -} +export const THEIA_EXT = 'theia-workspace'; +export const VSCODE_EXT = 'code-workspace'; @injectable() export class CommonWorkspaceUtils { @@ -43,20 +36,38 @@ export class CommonWorkspaceUtils { */ isWorkspaceFile(candidate: FileStat | URI): boolean { const uri = FileStat.is(candidate) ? candidate.resource : candidate; - return uri.path.ext === `.${THEIA_EXT}` || uri.path.ext === `.${VSCODE_EXT}`; + return uri.path.ext === `.${this.getWorkspaceExtension()}` || (this.isVSCodeWorkspaceSelectionEnabled() && uri.path.ext === `.${VSCODE_EXT}`); } isUntitledWorkspace(candidate?: URI): boolean { return !!candidate && this.isWorkspaceFile(candidate) && candidate.path.base.startsWith('Untitled'); } + /** + * Determines whether the workspace service allows to select `.code-workspace` as valid workspaces for the application. + * + * If this method returns `false`, only workspace file that fit the `CommonWorkspaceUtils#getWorkspaceExtension` return value can be selected. + */ + isVSCodeWorkspaceSelectionEnabled(): boolean { + return true; + } + + /** + * Returns the file extension used for all workspace files for this application. + * + * Returns `theia-workspace` by default. + */ + getWorkspaceExtension(): string { + return THEIA_EXT; + } + async getUntitledWorkspaceUri(configDirUri: URI, isAcceptable: (candidate: URI) => MaybePromise, warnOnHits?: () => unknown): Promise { const parentDir = configDirUri.resolve('workspaces'); let uri; let attempts = 0; do { attempts++; - uri = parentDir.resolve(`Untitled-${Math.round(Math.random() * 1000)}.${THEIA_EXT}`); + uri = parentDir.resolve(`Untitled-${Math.round(Math.random() * 1000)}.${this.getWorkspaceExtension()}`); if (attempts === 10) { warnOnHits?.(); }