diff --git a/packages/core/src/browser/widget-manager.ts b/packages/core/src/browser/widget-manager.ts index 4d2f343f8bcba..a9e232c3d9cfb 100644 --- a/packages/core/src/browser/widget-manager.ts +++ b/packages/core/src/browser/widget-manager.ts @@ -75,7 +75,7 @@ export class WidgetManager { protected _cachedFactories: Map; protected readonly widgets = new Map(); - protected readonly widgetPromises = new Map>(); + public readonly widgetPromises = new Map>(); protected readonly pendingWidgetPromises = new Map>(); @inject(ContributionProvider) @named(WidgetFactory) diff --git a/packages/core/src/browser/widget-open-handler.ts b/packages/core/src/browser/widget-open-handler.ts index bfe00ba905ba7..cfb95c24b2ee3 100644 --- a/packages/core/src/browser/widget-open-handler.ts +++ b/packages/core/src/browser/widget-open-handler.ts @@ -77,10 +77,34 @@ export abstract class WidgetOpenHandler implements OpenHan * Reject if the given options is not an widget options or a widget cannot be opened. */ async open(uri: URI, options?: WidgetOpenerOptions): Promise { + const widget = await this.getOrCreateWidget(uri, options); - await this.doOpen(widget, options); + const widgetId = this.getOpenedUntitledWidgetId(uri); + if (!widgetId) { + await this.doOpen(widget, options); + } else { + await this.shell.activateWidget(widgetId); + } return widget; } + + getOpenedUntitledWidgetId(uri: URI): string | undefined { + const keyUri = uri.path.toString(); + const untitledScheme = 'untitled'; + if (uri.scheme !== untitledScheme && this.widgetManager.widgetPromises) { + const existingWidgetsKeysArray = Array.from(this.widgetManager.widgetPromises.keys()); + for (let i = 0; i < existingWidgetsKeysArray.length; i++) { + const widgetKey = JSON.parse(existingWidgetsKeysArray[i]); + if (widgetKey.options && widgetKey.options.uri && widgetKey.options.uri.startsWith(untitledScheme)) { + if (widgetKey.options.uri.includes(keyUri)) { + return widgetKey.factoryId + ':' + widgetKey.options.uri; + } + } + } + } + return undefined; + } + protected async doOpen(widget: W, options?: WidgetOpenerOptions): Promise { const op: WidgetOpenerOptions = { mode: 'activate', diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index 7e717ec9b0575..c379d8391c349 100644 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -32,7 +32,7 @@ import { EditorManager } from '@theia/editor/lib/browser'; import { CodeEditorWidget } from '@theia/plugin-ext/lib/main/browser/menus/menus-contribution-handler'; import { TextDocumentShowOptions } from '@theia/plugin-ext/lib/common/plugin-api-rpc-model'; import { DocumentsMainImpl } from '@theia/plugin-ext/lib/main/browser/documents-main'; -import { createUntitledResource } from '@theia/plugin-ext/lib/main/browser/editor/untitled-resource'; +import { createUntitledURI } from '@theia/plugin-ext/lib/main/browser/editor/untitled-resource'; import { toDocumentSymbol } from '@theia/plugin-ext/lib/plugin/type-converters'; import { ViewColumn } from '@theia/plugin-ext/lib/plugin/types-impl'; import { WorkspaceCommands } from '@theia/workspace/lib/browser'; @@ -172,7 +172,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { * and apply actions only to them */ commands.registerCommand({ id: 'workbench.action.files.newUntitledFile' }, { - execute: () => open(this.openerService, createUntitledResource().uri) + execute: () => open(this.openerService, createUntitledURI()) }); commands.registerCommand({ id: 'workbench.action.files.openFile' }, { execute: () => commands.executeCommand(WorkspaceCommands.OPEN_FILE.id) diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index cf94a6cbe8aeb..91e8fa93a2eea 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -20,7 +20,7 @@ import { DisposableCollection, Disposable } from '@theia/core'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; import { RPCProtocol } from '../../common/rpc-protocol'; import { EditorModelService } from './text-editor-model-service'; -import { createUntitledResource } from './editor/untitled-resource'; +import { UntitledResourceResolver } from './editor/untitled-resource'; import { EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; import URI from '@theia/core/lib/common/uri'; import CodeURI from 'vscode-uri'; @@ -92,7 +92,8 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { rpc: RPCProtocol, private editorManager: EditorManager, private openerService: OpenerService, - private shell: ApplicationShell + private shell: ApplicationShell, + private untitledResourceResolver: UntitledResourceResolver ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT); @@ -167,7 +168,7 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { async $tryCreateDocument(options?: { language?: string; content?: string; }): Promise { const language = options && options.language; const content = options && options.content; - const resource = createUntitledResource(content, language); + const resource = await this.untitledResourceResolver.createUntitledResource(content, language); return monaco.Uri.parse(resource.uri.toString()); } diff --git a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts index d68b1067925c9..ca8c7b19c6d00 100644 --- a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts +++ b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts @@ -14,39 +14,89 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ResourceResolver, Resource } from '@theia/core'; +import { injectable, inject } from 'inversify'; +import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { Resource, ResourceResolver } from '@theia/core'; import URI from '@theia/core/lib/common/uri'; -import { injectable } from 'inversify'; import { Schemes } from '../../../common/uri-components'; +import { FileResource, FileResourceResolver } from '@theia/filesystem/lib/browser'; -const resources = new Map(); let index = 0; + @injectable() export class UntitledResourceResolver implements ResourceResolver { - resolve(uri: URI): Resource | Promise { - if (uri.scheme === Schemes.UNTITLED) { - return resources.get(uri.toString())!; + + @inject(FileResourceResolver) + protected readonly fileResourceResolver: FileResourceResolver; + + protected readonly resources = new Map(); + + async resolve(uri: URI): Promise { + if (uri.scheme !== Schemes.UNTITLED) { + throw new Error('The given uri is not untitled file uri: ' + uri); + } else { + const untitledResource = this.resources.get(uri.toString()); + if (!untitledResource) { + return this.createUntitledResource('', '', uri, this.fileResourceResolver); + } else { + return untitledResource; + } + } + } + + async createUntitledResource(content?: string, language?: string, uri?: URI, fileResourceResolver?: FileResourceResolver): Promise { + let extension; + if (language) { + for (const lang of monaco.languages.getLanguages()) { + if (lang.id === language) { + if (lang.extensions) { + extension = lang.extensions[0]; + break; + } + } + } } - throw new Error(`scheme ${uri.scheme} is not '${Schemes.UNTITLED}'`); + return new UntitledResource(this.resources, uri ? uri : new URI().withScheme(Schemes.UNTITLED).withPath(`/Untitled-${index++}${extension ? extension : ''}`), + content, fileResourceResolver); } } export class UntitledResource implements Resource { + private fileResource?: FileResource; - constructor(public uri: URI, private content?: string) { - resources.set(this.uri.toString(), this); + constructor(private resources: Map, public uri: URI, private content?: string, private fileResourceResolver?: FileResourceResolver) { + this.resources.set(this.uri.toString(), this); } - readContents(options?: { encoding?: string | undefined; } | undefined): Promise { - return Promise.resolve(this.content ? this.content : ''); + dispose(): void { + this.resources.delete(this.uri.toString()); } - dispose(): void { - resources.delete(this.uri.toString()); + async readContents(options?: { encoding?: string | undefined; } | undefined): Promise { + if (this.fileResource) { + return this.fileResource.readContents(options); + } else if (this.content) { + return Promise.resolve(this.content); + } else { + return ''; + } + } + + async saveContents(content: string, options?: { encoding?: string, overwriteEncoding?: string }): Promise { + if (this.fileResourceResolver) { + this.fileResource = await this.fileResourceResolver.resolve(new URI(this.uri.path.toString())); + await this.fileResource.saveContents(content, options); + } + } + + async saveContentChanges(changes: TextDocumentContentChangeEvent[], options?: { encoding?: string, overwriteEncoding?: string }): Promise { + if (this.fileResource) { + await this.fileResource.saveContentChanges(changes, options); + } } } -export function createUntitledResource(content?: string, language?: string): UntitledResource { +export function createUntitledURI(language?: string): URI { let extension; if (language) { for (const lang of monaco.languages.getLanguages()) { @@ -58,5 +108,5 @@ export function createUntitledResource(content?: string, language?: string): Unt } } } - return new UntitledResource(new URI().withScheme(Schemes.UNTITLED).withPath(`/Untitled-${index++}${extension ? extension : ''}`), content); + return new URI().withScheme(Schemes.UNTITLED).withPath(`/Untitled-${index++}${extension ? extension : ''}`); } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 2392961b80dcb..bd530d76fcfe2 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -47,6 +47,7 @@ import { OpenerService } from '@theia/core/lib/browser/opener-service'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service'; import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; +import { UntitledResourceResolver } from './editor/untitled-resource'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -73,7 +74,8 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const editorManager = container.get(EditorManager); const openerService = container.get(OpenerService); const shell = container.get(ApplicationShell); - const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell); + const untitledResourceResolver = container.get(UntitledResourceResolver); + const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver); rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain); const bulkEditService = container.get(MonacoBulkEditService);