import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di'; import { AppConfig, FILE_COMMANDS, IContextKey, IContextKeyService, IOpenerService, IScopedContextKeyService, OPEN_EDITORS_COMMANDS, PreferenceService, RecentFilesManager, ResizeEvent, ServiceNames, fastdom, getSlotLocation, toMarkdown, } from '@opensumi/ide-core-browser'; import { ResourceContextKey } from '@opensumi/ide-core-browser/lib/contextkey/resource'; import { IMergeEditorEditor, MergeEditorInputData } from '@opensumi/ide-core-browser/lib/monaco/merge-editor-widget'; import { CUSTOM_EDITOR_SCHEME, CommandService, ContributionProvider, Deferred, Disposable, Emitter, Event, Emitter as EventEmitter, IDisposable, ILogger, IReporterService, IStorage, MaybeNull, MessageType, OnEvent, REPORT_NAME, ReadyEvent, STORAGE_NAMESPACE, Schemes, StorageProvider, URI, WithEventBus, formatLocalize, getDebugLogger, isDefined, isUndefinedOrNull, isWindows, localize, makeRandomHexString, match, path, } from '@opensumi/ide-core-common'; import { IFileServiceClient } from '@opensumi/ide-file-service'; import * as monaco from '@opensumi/ide-monaco'; import { IDialogService, IMessageService, IWindowDialogService } from '@opensumi/ide-overlay'; import { Dimension } from '@opensumi/monaco-editor-core/esm/vs/base/browser/dom'; import { CursorStatus, EditorCollectionService, EditorGroupSplitAction, ICodeEditor, IDiffEditor, IDiffResource, IEditor, IEditorGroup, IEditorGroupState, IEditorOpenType, IOpenResourceResult, IResource, IResourceOpenOptions, IUntitledOptions, ResourceDecorationChangeEvent, ResourceService, SaveReason, WorkbenchEditorService, getSplitActionFromDragDrop, } from '../common'; import { IEditorDocumentModelRef } from '../common/editor'; import { IMultiDiffEditor } from '../common/multi-diff'; import { EditorDocumentModel } from './doc-model/editor-document-model'; import { IEditorDocumentModelService } from './doc-model/types'; import { EditorTabChangedError, isEditorError } from './error'; import { EditorGrid, IEditorGridState, IGridEditorGroup, SplitDirection } from './grid/grid.service'; import { AskSaveResult, BrowserEditorContribution, CodeEditorDidVisibleEvent, DragOverPosition, EditorActiveResourceStateChangedEvent, EditorComponentDisposeEvent, EditorComponentRegistry, EditorComponentRenderMode, EditorConfigurationChangedEvent, EditorGroupChangeEvent, EditorGroupCloseEvent, EditorGroupDisposeEvent, EditorGroupIndexChangedEvent, EditorGroupOpenEvent, EditorOpenType, EditorSelectionChangeEvent, EditorVisibleChangeEvent, GridResizeEvent, IEditorComponent, IEditorDocumentModel, IMergeEditorResource, RegisterEditorComponentEvent, ResoucesOfActiveComponentChangedEvent, ResourceOpenTypeChangedEvent, } from './types'; import { UntitledDocumentIdCounter } from './untitled-resource'; const MAX_CONFIRM_RESOURCES = 10; const couldRevive = (r: IResource): boolean => !!(r.supportsRevive && !r.deleted); @Injectable() export class WorkbenchEditorServiceImpl extends WithEventBus implements WorkbenchEditorService { editorGroups: EditorGroup[] = []; _onDidEditorGroupsChanged = new EventEmitter(); onDidEditorGroupsChanged: Event = this._onDidEditorGroupsChanged.event; private _sortedEditorGroups: EditorGroup[] | undefined = []; @Autowired(INJECTOR_TOKEN) private injector!: Injector; @Autowired(ResourceService) private resourceService: ResourceService; @Autowired(IFileServiceClient) protected readonly fileServiceClient: IFileServiceClient; @Autowired(AppConfig) private appConfig: AppConfig; private readonly _onActiveResourceChange = new EventEmitter>(); public readonly onActiveResourceChange: Event> = this._onActiveResourceChange.event; private readonly _onActiveEditorUriChange = new EventEmitter>(); public readonly onActiveEditorUriChange: Event> = this._onActiveEditorUriChange.event; private readonly _onCursorChange = new EventEmitter(); public readonly onCursorChange: Event = this._onCursorChange.event; public topGrid: EditorGrid; private _currentEditorGroup: IEditorGroup; private _onDidCurrentEditorGroupChanged = new EventEmitter(); onDidCurrentEditorGroupChanged: Event = this._onDidCurrentEditorGroupChanged.event; @Autowired(StorageProvider) getStorage: StorageProvider; @Autowired(IDialogService) protected dialogService: IDialogService; openedResourceState: IStorage; private _restoring = true; public contributionsReady = new Deferred(); private initializing: Promise; public editorContextKeyService: IScopedContextKeyService; @Autowired(IOpenerService) openner: IOpenerService; @Autowired(BrowserEditorContribution) private readonly contributions: ContributionProvider; @Autowired(IEditorDocumentModelService) protected documentModelManager: IEditorDocumentModelService; @Autowired(UntitledDocumentIdCounter) private untitledIndex: UntitledDocumentIdCounter; @Autowired(IWindowDialogService) private readonly windowDialogService: IWindowDialogService; private untitledCloseIndex: number[] = []; public gridReady = false; private _onDidGridReady = new Emitter(); public onDidGridReady = this._onDidGridReady.event; constructor() { super(); this.untitledIndex.update(); this.initialize(); } setEditorContextKeyService(contextKeyService: IScopedContextKeyService): void { this.editorContextKeyService = contextKeyService; } setCurrentGroup(editorGroup: EditorGroup) { if (editorGroup) { if (this._currentEditorGroup === editorGroup) { return; } this._currentEditorGroup = editorGroup; this._onActiveResourceChange.fire(editorGroup.currentResource); this.eventBus.fire( new EditorActiveResourceStateChangedEvent({ resource: editorGroup.currentResource, openType: editorGroup.currentOpenType, editorUri: this.currentEditor?.currentUri, }), ); this._onDidCurrentEditorGroupChanged.fire(this._currentEditorGroup); } } @OnEvent(EditorGroupChangeEvent) onEditorGroupChangeEvent(e: EditorGroupChangeEvent) { if (e.payload.group === this.currentEditorGroup) { this.eventBus.fire( new EditorActiveResourceStateChangedEvent({ resource: e.payload.newResource, openType: e.payload.newOpenType, editorUri: this.currentEditor?.currentUri, }), ); } } getAllOpenedUris() { const uris: URI[] = []; for (const group of this.editorGroups) { for (const resource of group.resources) { const index = uris.findIndex((u) => u.isEqual(resource.uri)); if (index === -1) { uris.push(resource.uri); } } } return uris; } async getAllOpenedDocuments() { const documents: IEditorDocumentModel[] = []; for (const group of this.editorGroups) { for (const resource of group.resources) { if (resource.uri.scheme !== 'file') { continue; } const index = documents.findIndex((u) => u.uri.isEqual(resource.uri)); if (index === -1) { const doc = await group.getDocumentModelRef(resource.uri); if (doc) { documents.push(doc.instance); } } } } return documents; } async save(uri: URI): Promise { if (!this.editorGroups.length) { return undefined; } try { for (const editorGroup of this.editorGroups) { const res = editorGroup.resources.find((resource) => resource.uri.isEqual(uri)); if (res) { await editorGroup.saveResource(res); } } return uri; } catch (error) { throw new Error(`Save Failed: ${error.message}`); } } private getDefaultSavePath(uri: URI): string { const defaultPath = uri.path.toString() !== '/' ? path.dirname(uri.path.toString()) : this.appConfig.workspaceDir; return isWindows ? defaultPath.replace(/\\/g, '/') : defaultPath; } async saveAs(uri: URI): Promise { if (!this._currentEditorGroup || this._currentEditorGroup.currentResource?.deleted) { return undefined; } try { const defaultPath = this.getDefaultSavePath(uri); const result = await this.windowDialogService.showSaveDialog({ saveLabel: 'SaveAs File:', showNameInput: true, defaultFileName: this._currentEditorGroup.currentResource?.name, defaultUri: URI.file(defaultPath), saveAs: true, }); return result; } catch (error) { throw new Error(`SaveAs Failed: ${error.message}`); } } async saveAll(includeUntitled?: boolean, reason?: SaveReason) { for (const editorGroup of this.editorGroups) { await editorGroup.saveAll(includeUntitled, reason); } } hasDirty(): boolean { for (const editorGroup of this.editorGroups) { if (editorGroup.hasDirty()) { return true; } } return false; } calcDirtyCount(): number { const countedUris = new Set(); return this.editorGroups.reduce((pre, cur) => pre + cur.calcDirtyCount(countedUris), 0); } private editorGroupIdGen = 0; createEditorGroup(): EditorGroup { const editorGroup = this.injector.get(EditorGroup, [this.generateRandomEditorGroupName(), this.editorGroupIdGen++]); this.editorGroups.push(editorGroup); const currentWatchDisposer = new Disposable( editorGroup.onDidEditorGroupBodyChanged(() => { if (editorGroup === this.currentEditorGroup) { if (!editorGroup.currentOpenType && editorGroup.currentResource) { // 暂时状态,不发事件 } else { this._onActiveResourceChange.fire(editorGroup.currentResource); } } }), editorGroup.onDidEditorFocusChange(() => { if (editorGroup === this.currentEditorGroup) { if (!editorGroup.currentOpenType && editorGroup.currentResource) { // 暂时状态,不发事件 } else { this._onActiveEditorUriChange.fire(editorGroup.currentOrPreviousFocusedEditor?.currentUri); } } }), ); editorGroup.addDispose({ dispose: () => { currentWatchDisposer.dispose(); }, }); const groupChangeDisposer = editorGroup.onDidEditorGroupTabChanged(() => { this.saveOpenedResourceState(); }); editorGroup.addDispose({ dispose: () => { groupChangeDisposer.dispose(); }, }); editorGroup.onCurrentEditorCursorChange((e) => { if (this._currentEditorGroup === editorGroup) { this._onCursorChange.fire(e); } }); return editorGroup; } /** * 随机生成一个不重复的editor Group */ private generateRandomEditorGroupName() { let name = makeRandomHexString(5); while (this.editorGroups.findIndex((g) => g.name === name) !== -1) { name = makeRandomHexString(5); } return name; } public initialize() { if (!this.initializing) { this.initializing = this.doInitialize(); } return this.initializing; } private async doInitialize() { this.openedResourceState = await this.initializeState(); await this.contributionsReady.promise; await this.restoreState(); this._currentEditorGroup = this.editorGroups[0]; } private async initializeState() { const state = await this.getStorage(STORAGE_NAMESPACE.WORKBENCH); return state; } public get currentEditor(): IEditor | null { return this.currentEditorGroup && this.currentEditorGroup.currentEditor; } public get currentCodeEditor(): ICodeEditor | null { return this.currentEditorGroup.currentCodeEditor; } public get currentEditorGroup(): EditorGroup { return this._currentEditorGroup as any; } public get currentOrPreviousFocusedEditor(): IEditor | null { return this.currentEditorGroup && this.currentEditorGroup.currentOrPreviousFocusedEditor; } async open(uri: URI, options?: IResourceOpenOptions) { await this.initialize(); let group = this.currentEditorGroup; let groupIndex: number | undefined; if (options && typeof options.groupIndex !== 'undefined') { groupIndex = options.groupIndex; } else if (options && options.relativeGroupIndex) { groupIndex = this.currentEditorGroup.index + options.relativeGroupIndex; } if (typeof groupIndex === 'number' && groupIndex >= 0) { if (groupIndex >= this.editorGroups.length) { return group.open(uri, Object.assign({}, options, { split: EditorGroupSplitAction.Right })); } else { group = this.sortedEditorGroups[groupIndex] || this.currentEditorGroup; } } return group.open(uri, options); } async openUris(uris: URI[]) { await this.initialize(); await this.currentEditorGroup.openUris(uris); return; } getEditorGroup(name: string): EditorGroup | undefined { return this.editorGroups.find((g) => g.name === name); } get currentResource(): MaybeNull { if (!this.currentEditorGroup) { return null; } return this.currentEditorGroup.currentResource; } removeGroup(group: EditorGroup) { const index = this.editorGroups.findIndex((e) => e === group); if (index !== -1) { if (this.editorGroups.length === 1) { return; } this.editorGroups.splice(index, 1); if (this.currentEditorGroup === group) { this.setCurrentGroup(this.editorGroups[0]); } for (let i = index; i < this.editorGroups.length; i++) { this.eventBus.fire( new EditorGroupIndexChangedEvent({ group: this.editorGroups[i], index: i, }), ); } } } public async saveOpenedResourceState() { if (this._restoring) { return; } const state: IEditorGridState = this.topGrid.serialize()!; await this.openedResourceState.set('grid', state); } prepareContextKeyService() { // contextKeys const getLanguageFromModel = (uri: URI) => { let result: string | null = null; const modelRef = this.documentModelManager.getModelDescription(uri, 'resourceContextKey'); if (modelRef) { result = modelRef.languageId; } return result; }; const resourceContext = new ResourceContextKey(this.editorContextKeyService, (uri: URI) => { const res = getLanguageFromModel(uri); if (res) { return res!; } else { return getLanguageFromModel(uri); } }); this.onActiveResourceChange((resource) => { if (this.currentEditor && this.currentEditor.currentUri) { resourceContext.set(this.currentEditor.currentUri); } else { if (resource) { resourceContext.set(resource.uri); } else { resourceContext.reset(); } } }); if (this.currentEditor && this.currentEditor.currentUri) { resourceContext.set(this.currentEditor.currentUri); } else { if (this.currentResource) { resourceContext.set(this.currentResource.uri); } else { resourceContext.reset(); } } } onDomCreated(domNode: HTMLElement) { if (this.editorContextKeyService) { this.editorContextKeyService.attachToDomNode(domNode); } } private notifyGroupChanged() { this._sortedEditorGroups = undefined; this._onDidEditorGroupsChanged.fire(); } public async restoreState() { let state: IEditorGridState = { editorGroup: { uris: [], previewIndex: -1 } }; if (!this.appConfig.disableRestoreEditorGroupState) { state = this.openedResourceState.get('grid', state); } this.topGrid = new EditorGrid(); this.addDispose(this.topGrid); this.addDispose( this.topGrid.onDidGridAndDesendantStateChange(() => { this._sortedEditorGroups = undefined; this._onDidEditorGroupsChanged.fire(); }), ); const editorRestorePromises = []; const promise = this.topGrid .deserialize(state, () => this.createEditorGroup(), editorRestorePromises) .then(() => { if (this.topGrid.children.length === 0 && !this.topGrid.editorGroup) { this.topGrid.setEditorGroup(this.createEditorGroup()); } this.gridReady = true; this._onDidGridReady.fire(); this.notifyGroupChanged(); }); Promise.all(editorRestorePromises).then(() => { this._restoring = false; for (const contribution of this.contributions.getContributions()) { if (contribution.onDidRestoreState) { contribution.onDidRestoreState(); } } }); return promise; } async closeAll(uri?: URI, force?: boolean) { for (const group of this.editorGroups.slice(0)) { if (uri) { await group.close(uri, { force }); } else { await group.closeAll(); } } } /** * Return true in order to prevent exit. */ async closeAllOnlyConfirmOnce() { const resources = [] as IResource[]; for (const group of this.editorGroups) { for (const resource of group.resources) { resources.push(resource); } } const shouldClose = await Promise.all( resources.map(async (resource) => ({ shouldClose: await this.resourceService.shouldCloseResourceWithoutConfirm(resource), resource, })), ); const toClose = shouldClose.filter((v) => v.shouldClose); if (toClose.length === 0) { return false; } // 询问用户是否保存 const buttons = { [localize('file.prompt.dontSave', "Don't Save")]: AskSaveResult.REVERT, [localize('file.prompt.save', 'Save')]: AskSaveResult.SAVE, [localize('file.prompt.cancel', 'Cancel')]: AskSaveResult.CANCEL, }; const files = toClose.slice(0, MAX_CONFIRM_RESOURCES); let filesDetail = files.map((v) => v.resource.name).join('、'); if (toClose.length > MAX_CONFIRM_RESOURCES) { if (toClose.length - MAX_CONFIRM_RESOURCES === 1) { filesDetail += localize('file.prompt.more.one'); } else { filesDetail += formatLocalize('file.prompt.more.number', toClose.length - MAX_CONFIRM_RESOURCES); } } const selection = await this.dialogService.open({ message: toMarkdown(formatLocalize('saveNFilesChangesMessage', toClose.length, filesDetail), this.openner), type: MessageType.Info, buttons: Object.keys(buttons), }); const result = buttons[selection!]; if (result === AskSaveResult.SAVE) { await Promise.all(toClose.map((v) => this.resourceService.close?.(v.resource, AskSaveResult.SAVE))); return false; } else if (result === AskSaveResult.REVERT) { await Promise.all(toClose.map((v) => this.resourceService.close?.(v.resource, AskSaveResult.REVERT))); return false; } return true; } async close(uri: URI, force?: boolean) { return this.closeAll(uri, force); } get sortedEditorGroups() { if (!this._sortedEditorGroups) { this._sortedEditorGroups = []; this.topGrid.sortEditorGroups(this._sortedEditorGroups); } return this._sortedEditorGroups; } @OnEvent(EditorGroupCloseEvent) handleOnCloseUntitledResource(e: EditorGroupCloseEvent) { if (e.payload.resource.uri.scheme === Schemes.untitled) { const { index } = e.payload.resource.uri.getParsedQuery(); this.untitledCloseIndex.push(parseInt(index, 10)); // 升序排序,每次可以去到最小的 index this.untitledCloseIndex.sort((a, b) => a - b); } } private createUntitledURI() { // 优先从已删除的 index 中获取 const index = this.untitledCloseIndex.shift() || this.untitledIndex.id; return new URI() .withScheme(Schemes.untitled) .withQuery(`name=${UntitledDocumentIdCounter.UNTITLED_FILE_NAME_PREFIX}${index}&index=${index}`); } createUntitledResource( options: IUntitledOptions = { uri: this.createUntitledURI(), }, ) { return this.open(options.uri, { preview: false, focus: true, ...options.resourceOpenOptions, }); } } export interface IEditorCurrentState { currentResource: IResource; currentOpenType: IEditorOpenType; } /** * Editor Group是一个可视的编辑区域 * 它由tab,editor,diff-editor,富组件container组成 */ @Injectable({ multiple: true }) export class EditorGroup extends WithEventBus implements IGridEditorGroup { @Autowired() collectionService!: EditorCollectionService; @Autowired() resourceService: ResourceService; @Autowired() editorComponentRegistry: EditorComponentRegistry; @Autowired(WorkbenchEditorService) workbenchEditorService: WorkbenchEditorServiceImpl; @Autowired(IEditorDocumentModelService) protected documentModelManager: IEditorDocumentModelService; @Autowired(CommandService) private commands: CommandService; @Autowired(PreferenceService) protected readonly preferenceService: PreferenceService; @Autowired(RecentFilesManager) private readonly recentFilesManager: RecentFilesManager; @Autowired(IMessageService) private messageService: IMessageService; @Autowired(IReporterService) private reporterService: IReporterService; @Autowired(AppConfig) config: AppConfig; @Autowired(IOpenerService) private readonly openerService: IOpenerService; @Autowired(ILogger) logger: ILogger; codeEditor!: ICodeEditor; diffEditor!: IDiffEditor; multiDiffEditor!: IMultiDiffEditor; mergeEditor!: IMergeEditorEditor; private openingPromise: Map> = new Map(); _onDidEditorFocusChange = this.registerDispose(new EventEmitter()); onDidEditorFocusChange: Event = this._onDidEditorFocusChange.event; /** * 当编辑器的tab部分发生变更 */ _onDidEditorGroupTabChanged = new EventEmitter(); onDidEditorGroupTabChanged: Event = this._onDidEditorGroupTabChanged.event; /** * 当编辑器的tab部分发生变更 */ _onDidEditorGroupTabOperation = new EventEmitter(); onDidEditorGroupTabOperation: Event = this._onDidEditorGroupTabOperation.event; /** * 当编辑器的主体部分发生变更 */ _onDidEditorGroupBodyChanged = new EventEmitter(); onDidEditorGroupBodyChanged: Event = this._onDidEditorGroupBodyChanged.event; /** * 当编辑器有内容处于加载状态 */ _onDidEditorGroupContentLoading = new EventEmitter(); onDidEditorGroupContentLoading: Event = this._onDidEditorGroupContentLoading.event; /** * 每个group只能有一个preview */ public previewURI: URI | null = null; /** * 当前打开的所有resource */ resources: IResource[] = []; resourceStatus: Map> = new Map(); _currentResource: IResource | null; _currentOpenType: IEditorOpenType | null; /** * 当前resource的打开方式 */ private cachedResourcesActiveOpenTypes = new Map(); private cachedResourcesOpenTypes = new Map(); availableOpenTypes: IEditorOpenType[] = []; activeComponents = new Map(); activateComponentsProps = new Map(); public grid: EditorGrid; private holdDocumentModelRefs: Map = new Map(); private readonly toDispose: monaco.IDisposable[] = []; private _contextKeyService: IContextKeyService; private _resourceContext: ResourceContextKey; private _editorLangIDContextKey: IContextKey; private _activeEditorIsDirtyContextKey: IContextKey; private _isInDiffEditorContextKey: IContextKey; private _isInMultiDiffEditorContextKey: IContextKey; private _diffResourceContextKey: ResourceContextKey; private _isInDiffRightEditorContextKey: IContextKey; private _isInEditorComponentContextKey: IContextKey; private _prevDomHeight = 0; private _prevDomWidth = 0; // 当前为EditorComponent,且monaco光标变化时触发 private _onCurrentEditorCursorChange = new EventEmitter(); public onCurrentEditorCursorChange = this._onCurrentEditorCursorChange.event; private resourceOpenHistory: URI[] = []; private _domNode: MaybeNull = null; private _diffEditorDomNode: MaybeNull = null; private _diffEditorDomNodeAttached = false; private _multiFileDiffEditorDomNode: MaybeNull = null; private _multiFileDiffEditorDomNodeAttached = false; private _mergeEditorDomNode: MaybeNull = null; private _mergeEditorDomNodeAttached = false; private codeEditorReady = new ReadyEvent(); private diffEditorReady = new ReadyEvent(); private diffEditorDomReady = new ReadyEvent(); private multiDiffEditorDomReady = new ReadyEvent(); private mergeEditorReady = new ReadyEvent(); private mergeEditorDomReady = new ReadyEvent(); private _restoringState = false; private updateContextKeyWhenEditorChangesFocusDisposer: IDisposable; private _currentOrPreviousFocusedEditor: IEditor | null; private _multiDiffEditorDomNode: HTMLElement | null | undefined; private _multiDiffEditorDomNodeAttached = false; constructor(public readonly name: string, public readonly groupId: number) { super(); this.eventBus.onDirective( ResizeEvent.createDirective(getSlotLocation('@opensumi/ide-editor', this.config.layoutConfig)), () => { this.doLayoutEditors(); }, ); this.eventBus.on(GridResizeEvent, (e: GridResizeEvent) => { if (e.payload.gridId === this.grid.uid) { this.doLayoutEditors(); } }); this.eventBus.on(EditorComponentDisposeEvent, (e: EditorComponentDisposeEvent) => { this.activeComponents.delete(e.payload); this.activateComponentsProps.delete(e.payload); }); this.listenToExplorerAutoRevealConfig(); } private explorerAutoRevealConfig: boolean; private listenToExplorerAutoRevealConfig() { this.explorerAutoRevealConfig = !!this.preferenceService.get('explorer.autoReveal'); this.disposables.push( this.preferenceService.onPreferenceChanged((change) => { if (change.preferenceName === 'explorer.autoReveal') { this.explorerAutoRevealConfig = change.newValue; } }), ); } attachDiffEditorDom(domNode: HTMLElement | null | undefined) { if (!this._diffEditorDomNodeAttached) { this._diffEditorDomNode = domNode; this.diffEditorDomReady.ready(); this._diffEditorDomNodeAttached = true; } } attachMergeEditorDom(domNode: HTMLElement | null | undefined) { if (!this._mergeEditorDomNodeAttached) { this._mergeEditorDomNode = domNode; this.mergeEditorDomReady.ready(); this._mergeEditorDomNodeAttached = true; } } attachToDom(domNode: HTMLElement | null | undefined) { this._domNode = domNode; if (domNode) { (this.contextKeyService as IScopedContextKeyService).attachToDomNode(domNode); this.layoutEditors(); } } layoutEditors() { fastdom.measure(() => { if (this._domNode) { const currentWidth = this._domNode.offsetWidth; const currentHeight = this._domNode.offsetHeight; if (currentWidth !== this._prevDomWidth || currentHeight !== this._prevDomHeight) { this.doLayoutEditors(); } this._prevDomWidth = currentWidth; this._prevDomHeight = currentHeight; } }); } private _layoutEditorWorker() { if (!this.currentOpenType) { return; } switch (this.currentOpenType.type) { case EditorOpenType.code: if (this.codeEditor) { this.codeEditor.layout(); } break; case EditorOpenType.diff: if (this.diffEditor) { this.diffEditor.layout(); } break; case EditorOpenType.multiDiff: if (this.multiDiffEditor && this._multiDiffEditorDomNodeAttached) { this.multiDiffEditor.layout( new Dimension(this._multiDiffEditorDomNode!.offsetWidth, this._multiDiffEditorDomNode!.offsetHeight), ); } break; default: break; } } protected _measure: IDisposable | undefined; doLayoutEditors() { if (this._measure) { this._measure.dispose(); } this._measure = fastdom.mutate(() => { this._layoutEditorWorker(); }); } setContextKeys() { if (!this._resourceContext) { const getLanguageFromModel = (uri: URI) => { let result: string | null = null; const modelRef = this.documentModelManager.getModelDescription(uri, 'resourceContextKey'); if (modelRef) { result = modelRef.languageId; } return result; }; this._resourceContext = new ResourceContextKey(this.contextKeyService, (uri: URI) => getLanguageFromModel(uri)); this._diffResourceContextKey = new ResourceContextKey( this.contextKeyService, (uri: URI) => getLanguageFromModel(uri), 'diffResource', ); this._editorLangIDContextKey = this.contextKeyService.createKey('editorLangId', ''); this._isInDiffEditorContextKey = this.contextKeyService.createKey('isInDiffEditor', false); this._isInMultiDiffEditorContextKey = this.contextKeyService.createKey('isInMultiDiffEditor', false); this._activeEditorIsDirtyContextKey = this.contextKeyService.createKey('activeEditorIsDirty', false); this._isInDiffRightEditorContextKey = this.contextKeyService.createKey('isInDiffRightEditor', false); this._isInEditorComponentContextKey = this.contextKeyService.createKey('inEditorComponent', false); } if (this.currentOrPreviousFocusedEditor && this.currentOrPreviousFocusedEditor.currentUri) { this._resourceContext.set(this.currentOrPreviousFocusedEditor.currentUri); if (this.currentOrPreviousFocusedEditor.currentDocumentModel) { this._editorLangIDContextKey.set(this.currentOrPreviousFocusedEditor.currentDocumentModel.languageId); } } else if (this.currentEditor && this.currentEditor.currentUri) { this._resourceContext.set(this.currentEditor.currentUri); if (this.currentEditor.currentDocumentModel) { this._editorLangIDContextKey.set(this.currentEditor.currentDocumentModel.languageId); } } else { if (this.currentResource) { this._resourceContext.set(this.currentResource.uri); } else { this._resourceContext.reset(); } this._editorLangIDContextKey.reset(); } this._activeEditorIsDirtyContextKey.set(this.activeEditorIsDirty()); this._isInDiffEditorContextKey.set(this.isDiffEditorMode()); this._isInMultiDiffEditorContextKey.set(this.isMultiDiffEditorMode()); // 没有 focus 的时候默认添加在 RightDiffEditor this._isInDiffRightEditorContextKey.set(this.isDiffEditorMode()); this._isInEditorComponentContextKey.set(this.isComponentMode()); if (this.isDiffEditorMode()) { this._diffResourceContextKey.set(this.currentResource?.uri); } this.updateContextKeyWhenDiffEditorChangesFocus(); } private updateContextKeyWhenDiffEditorChangesFocus() { if (this.updateContextKeyWhenEditorChangesFocusDisposer || !this.diffEditor) { return; } const emitIfNoEditorFocused = () => { if (!this.currentFocusedEditor) { this.setContextKeys(); this._onDidEditorFocusChange.fire(); } }; this.updateContextKeyWhenEditorChangesFocusDisposer = new Disposable( this.diffEditor.modifiedEditor.onFocus(() => { this._currentOrPreviousFocusedEditor = this.diffEditor.modifiedEditor; this.setContextKeys(); this._onDidEditorFocusChange.fire(); }), this.diffEditor.originalEditor.onFocus(() => { this._currentOrPreviousFocusedEditor = this.diffEditor.originalEditor; this.setContextKeys(); this._onDidEditorFocusChange.fire(); }), this.codeEditor.onFocus(() => { if (this.codeEditor.currentUri) { this.locateInFileTree(this.codeEditor.currentUri); } this._currentOrPreviousFocusedEditor = this.codeEditor; this.setContextKeys(); this._onDidEditorFocusChange.fire(); }), this.codeEditor.onBlur(emitIfNoEditorFocused), this.diffEditor.originalEditor.onBlur(emitIfNoEditorFocused), this.diffEditor.modifiedEditor.onBlur(emitIfNoEditorFocused), ); this.addDispose(this.updateContextKeyWhenEditorChangesFocusDisposer); } get contextKeyService() { if (!this._contextKeyService) { this._contextKeyService = this.workbenchEditorService.editorContextKeyService.createScoped(); } return this._contextKeyService; } get index(): number { return this.workbenchEditorService.sortedEditorGroups.indexOf(this); } @OnEvent(ResourceDecorationChangeEvent) onResourceDecorationChangeEvent(e: ResourceDecorationChangeEvent) { if (e.payload.decoration.dirty) { if (this.previewURI && this.previewURI.isEqual(e.payload.uri)) { this.pinPreviewed(); } } const existingResource = this.resources.find((r) => r.uri.isEqual(e.payload.uri)); if (existingResource) { this.notifyTabChanged(); } } resetOpenType() { this._currentOpenType = null; } @OnEvent(ResourceOpenTypeChangedEvent) oResourceOpenTypeChangedEvent(e: ResourceOpenTypeChangedEvent) { const uri = e.payload; if (this.cachedResourcesOpenTypes.has(uri.toString())) { this.cachedResourcesOpenTypes.delete(uri.toString()); } if (this.currentResource && this.currentResource.uri.isEqual(uri)) { this.resetOpenType(); this.notifyBodyChanged(); this.displayResourceComponent(this.currentResource, {}); } } @OnEvent(RegisterEditorComponentEvent) async onRegisterEditorComponentEvent() { if (this.currentResource) { const resource = this.currentResource; const openTypes = await this.editorComponentRegistry.resolveEditorComponent(resource); if (this.currentResource === resource) { this.availableOpenTypes = openTypes; this.cachedResourcesOpenTypes.set(resource.uri.toString(), openTypes); } } } pinPreviewed(uri?: URI) { const previous = this.previewURI; if (uri === undefined) { this.previewURI = null; } else if (this.previewURI && this.previewURI.isEqual(uri)) { this.previewURI = null; } if (previous !== this.previewURI) { this.notifyTabChanged(); } } private notifyTabChanged() { if (this._restoringState) { return; } this._onDidEditorGroupTabChanged.fire(); } private notifyBodyChanged() { this._onDidEditorGroupBodyChanged.fire(); } private notifyTabLoading(resource: IResource) { this._onDidEditorGroupContentLoading.fire(resource); } get currentEditor(): IEditor | null { if (this.currentOpenType) { if (this.currentOpenType.type === EditorOpenType.code) { return this.codeEditor; } else if (this.currentOpenType.type === EditorOpenType.diff) { return this.diffEditor.modifiedEditor; } else { return null; } } else { return null; } } get currentOrPreviousFocusedEditor(): IEditor | null { return this._currentOrPreviousFocusedEditor || this.currentEditor; } get currentFocusedEditor() { if (this.currentOpenType) { if (this.currentOpenType.type === EditorOpenType.code) { if (this.codeEditor.monacoEditor.hasWidgetFocus()) { return this.codeEditor; } } else if (this.currentOpenType.type === EditorOpenType.diff) { if (this.diffEditor.modifiedEditor.monacoEditor.hasTextFocus()) { return this.diffEditor.modifiedEditor; } else if (this.diffEditor.originalEditor.monacoEditor.hasTextFocus()) { return this.diffEditor.originalEditor; } if (this.diffEditor.modifiedEditor.monacoEditor.hasWidgetFocus()) { return this.diffEditor.modifiedEditor; } else if (this.diffEditor.originalEditor.monacoEditor.hasWidgetFocus()) { return this.diffEditor.originalEditor; } } } return null; } get currentCodeEditor(): ICodeEditor | null { if (this.currentOpenType) { if (this.currentOpenType.type === EditorOpenType.code) { return this.codeEditor; } else { return null; } } else { return null; } } createEditor(dom: HTMLElement) { this.codeEditor = this.collectionService.createCodeEditor( dom, {}, { [ServiceNames.CONTEXT_KEY_SERVICE]: this.contextKeyService.contextKeyService, }, ); setTimeout(() => { this.doLayoutEditors(); }); this.addDispose( this.codeEditor.onRefOpen(() => { this.doLayoutEditors(); }), ); this.toDispose.push( this.codeEditor.onCursorPositionChanged((e) => { this._onCurrentEditorCursorChange.fire(e); }), ); this.toDispose.push( this.codeEditor.onSelectionsChanged((e) => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.code) { this.eventBus.fire( new EditorSelectionChangeEvent({ group: this, resource: this.currentResource!, selections: e.selections, source: e.source, editorUri: this.codeEditor.currentUri!, }), ); } }), ); this.toDispose.push( this.codeEditor.onVisibleRangesChanged((e) => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.code) { this.eventBus.fire( new EditorVisibleChangeEvent({ group: this, resource: this.currentResource!, visibleRanges: e, editorUri: this.codeEditor.currentUri!, }), ); } }), ); this.toDispose.push( this.codeEditor.onConfigurationChanged(() => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.code) { this.eventBus.fire( new EditorConfigurationChangedEvent({ group: this, resource: this.currentResource!, editorUri: this.codeEditor.currentUri!, }), ); } }), ); this.eventBus.fire( new CodeEditorDidVisibleEvent({ groupName: this.name, type: EditorOpenType.code, editorId: this.codeEditor.getId(), }), ); this.toDispose.push( this.codeEditor.monacoEditor.onDidChangeModelContent(() => { this._activeEditorIsDirtyContextKey.set(this.activeEditorIsDirty()); }), ); this.codeEditorReady.ready(); } createMergeEditor(dom: HTMLElement) { this.mergeEditor = this.collectionService.createMergeEditor( dom, {}, { [ServiceNames.CONTEXT_KEY_SERVICE]: this.contextKeyService.contextKeyService, }, ); this.mergeEditorReady.ready(); } createDiffEditor(dom: HTMLElement) { this.diffEditor = this.collectionService.createDiffEditor( dom, {}, { [ServiceNames.CONTEXT_KEY_SERVICE]: this.contextKeyService.contextKeyService, }, ); setTimeout(() => { this.doLayoutEditors(); }); this.diffEditor.onRefOpen(() => { this.doLayoutEditors(); }); this.addDiffEditorEventListeners(this.diffEditor.originalEditor, 'original'); this.addDiffEditorEventListeners(this.diffEditor.modifiedEditor, 'modified'); this.eventBus.fire( new CodeEditorDidVisibleEvent({ groupName: this.name, type: EditorOpenType.diff, editorId: this.diffEditor.modifiedEditor.getId(), }), ); this.eventBus.fire( new CodeEditorDidVisibleEvent({ groupName: this.name, type: EditorOpenType.diff, editorId: this.diffEditor.originalEditor.getId(), }), ); this.diffEditorReady.ready(); } private addDiffEditorEventListeners(editor: IEditor, side?: 'modified' | 'original') { this.toDispose.push( editor.onSelectionsChanged((e) => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.diff) { this.eventBus.fire( new EditorSelectionChangeEvent({ group: this, resource: this.currentResource!, selections: e.selections, source: e.source, editorUri: URI.from(editor.monacoEditor.getModel()!.uri!), side, }), ); } }), ); this.toDispose.push( editor.onVisibleRangesChanged((e) => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.diff) { this.eventBus.fire( new EditorVisibleChangeEvent({ group: this, resource: this.currentResource!, visibleRanges: e, editorUri: editor.currentUri!, }), ); } }), ); this.toDispose.push( editor.onConfigurationChanged(() => { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.diff) { this.eventBus.fire( new EditorConfigurationChangedEvent({ group: this, resource: this.currentResource!, editorUri: editor.currentUri!, }), ); } }), ); } async split(action: EditorGroupSplitAction, uri: URI, options?: IResourceOpenOptions) { const editorGroup = this.workbenchEditorService.createEditorGroup(); const direction = action === EditorGroupSplitAction.Left || action === EditorGroupSplitAction.Right ? SplitDirection.Horizontal : SplitDirection.Vertical; const before = action === EditorGroupSplitAction.Left || action === EditorGroupSplitAction.Top ? true : false; this.grid.split(direction, editorGroup, before); // 对于同一个编辑器分栏的场景,希望保留原本的滚动状态,与 VS Code 保持一致 if (options && !options.scrollTop) { options.scrollTop = this.currentEditor?.monacoEditor.getScrollTop(); } if (options && !options.scrollLeft) { options.scrollLeft = this.currentEditor?.monacoEditor.getScrollLeft(); } if (options && !options?.range) { const selection = this.currentCodeEditor?.monacoEditor.getSelection(); if (selection) { options.range = new monaco.Range( selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn, ); } } return editorGroup.open(uri, { ...options, preview: false, revealRangeInCenter: false }); } async open(uri: URI, options: IResourceOpenOptions = {}): Promise { if (uri.scheme === Schemes.file) { // 只记录 file 类型的 this.recentFilesManager.setMostRecentlyOpenedFile!(uri.withoutFragment().toString()); } if (options && options.split) { return this.split(options.split, uri, Object.assign({}, options, { split: undefined, preview: false })); } if (!this.openingPromise.has(uri.toString())) { const promise = this.doOpen(uri, options); this.openingPromise.set(uri.toString(), promise); promise.then( () => { this.openingPromise.delete(uri.toString()); }, () => { this.openingPromise.delete(uri.toString()); }, ); } const previewMode = this.preferenceService.get('editor.previewMode') && (isUndefinedOrNull(options.preview) ? true : options.preview); if (!previewMode) { this.openingPromise.get(uri.toString())!.then(() => { this.pinPreviewed(uri); }); } return this.openingPromise.get(uri.toString())!; } async pin(uri: URI) { return this.pinPreviewed(uri); } protected getPreventScrollOption(options: IResourceOpenOptions = {}) { const preventScroll = options.preventScroll ?? this.preferenceService.get('editor.preventScrollAfterFocused'); return preventScroll; } async doOpen( uri: URI, options: IResourceOpenOptions = {}, ): Promise<{ group: IEditorGroup; resource: IResource } | false> { if (!this.resourceService.handlesUri(uri)) { this.openerService.open(uri); return false; } let resourceReady: Deferred | undefined; try { const previewMode = this.preferenceService.get('editor.previewMode') && (isUndefinedOrNull(options.preview) ? true : options.preview); if ( this.currentResource && this.currentResource.uri.isEqual(uri) && // 当不存在 forceOpenType 或打开类型与当前打开类型符合时,才能说明是当前打开的 Reource 资源 (!options.forceOpenType || options.forceOpenType.type === this.currentOpenType?.type) ) { // 就是当前打开的 Resource if (options.focus && this.currentEditor) { const preventScroll = this.getPreventScrollOption(options); this._domNode?.focus({ preventScroll, }); this.currentEditor.monacoEditor.focus(); } if (options.range && this.currentEditor) { this.currentEditor.monacoEditor.setSelection(options.range as monaco.IRange); setTimeout(() => { this.currentEditor?.monacoEditor.revealRangeInCenter(options.range as monaco.IRange, 0); }, 0); } // 执行定位逻辑 this.locationInTree(options, uri); this.notifyTabChanged(); return { group: this, resource: this.currentResource, }; } else { const oldOpenType = this._currentOpenType; const oldResource = this._currentResource; let tabOperationToFire: IResourceTabOperation | null = null; let resource: IResource | null | undefined = this.resources.find((r) => r.uri.toString() === uri.toString()); if (!resource) { // open new resource resource = await this.resourceService.getResource(uri); if (!resource) { throw new Error('This uri cannot be opened!: ' + uri); } if (resource.deleted) { if (options.deletedPolicy === 'fail') { throw new Error('resource deleted ' + uri); } else if (options.deletedPolicy === 'skip') { return false; } } if (options && options.label) { resource.name = options.label; } if (options && isDefined(options.supportsRevive)) { resource.supportsRevive = options.supportsRevive; } let replaceResource: IResource | null = null; if (options && options.index !== undefined && options.index < this.resources.length) { replaceResource = this.resources[options.index]; this.resources.splice(options.index, 0, resource); tabOperationToFire = { type: 'open', resource, index: options.index, }; } else { if (this.currentResource) { const currentIndex = this.resources.indexOf(this.currentResource); this.resources.splice(currentIndex + 1, 0, resource); tabOperationToFire = { type: 'open', resource, index: currentIndex + 1, }; replaceResource = this.currentResource; } else { this.resources.push(resource); tabOperationToFire = { type: 'open', resource, index: this.resources.length - 1, }; } } if (previewMode) { if (this.previewURI) { await this.close(this.previewURI, { treatAsNotCurrent: true, force: options.forceClose }); } this.previewURI = resource.uri; } if (options.replace && replaceResource) { await this.close(replaceResource.uri, { treatAsNotCurrent: true, force: options.forceClose }); } } if (options.backend) { this.notifyTabChanged(); return false; } if (oldResource && this.resourceOpenHistory[this.resourceOpenHistory.length - 1] !== oldResource.uri) { this.resourceOpenHistory.push(oldResource.uri); const oldResourceSelections = this.currentCodeEditor?.getSelections(); if (oldResourceSelections && oldResourceSelections.length > 0) { this.recentFilesManager.updateMostRecentlyOpenedFile(oldResource.uri.toString(), { lineNumber: oldResourceSelections[0].selectionStartLineNumber, column: oldResourceSelections[0].selectionStartColumn, }); } } this._currentOpenType = null; this._currentResource = resource; // 只有真正打开的文件才会走到这里,backend模式的只更新了tab,文件内容并未加载 const reportTimer = this.reporterService.time(REPORT_NAME.EDITOR_REACTIVE); resourceReady = new Deferred(); this.resourceStatus.set(resource, resourceReady.promise); // 超过60ms loading时间的才展示加载 const delayTimer = setTimeout(() => { this.notifyTabLoading(resource!); }, 60); this.notifyTabChanged(); this.notifyBodyChanged(); try { await this.displayResourceComponent(resource, options); } finally { if (tabOperationToFire) { this._onDidEditorGroupTabOperation.fire(tabOperationToFire); } } this._currentOrPreviousFocusedEditor = this.currentEditor; clearTimeout(delayTimer); resourceReady.resolve(); reportTimer.timeEnd(resource.uri.toString()); this._onDidEditorFocusChange.fire(); this.setContextKeys(); this.eventBus.fire( new EditorGroupOpenEvent({ group: this, resource, }), ); // 执行定位逻辑 this.locationInTree(options, uri); this.eventBus.fire( new EditorGroupChangeEvent({ group: this, newOpenType: this.currentOpenType, newResource: this.currentResource, oldOpenType, oldResource, }), ); return { group: this, resource, }; } } catch (e) { getDebugLogger().error(e); resourceReady && resourceReady.reject(); if (!isEditorError(e, EditorTabChangedError)) { this.messageService.error(formatLocalize('editor.failToOpen', uri.displayName, e.message), [], true); } return false; // todo 给用户显示error } } private locationInTree(options: IResourceOpenOptions, uri: URI) { if (!options?.backend) { if (!options?.disableNavigate) { this.locateInFileTree(uri); } if (!options.disableNavigateOnOpendEditor) { this.locateInOpenedEditor(uri); } } } private locateInFileTree(uri: URI) { if (this.explorerAutoRevealConfig) { this.commands.tryExecuteCommand(FILE_COMMANDS.LOCATION.id, uri); } } private locateInOpenedEditor(uri: URI) { if (this.explorerAutoRevealConfig) { this.commands.tryExecuteCommand(OPEN_EDITORS_COMMANDS.LOCATION.id, uri); } } async openUris(uris: URI[]): Promise { for (const uri of uris) { await this.open(uri); } } async getDocumentModelRef(uri: URI): Promise { const key = uri.toString(); if (!this.holdDocumentModelRefs.has(key)) { this.holdDocumentModelRefs.set( key, await this.documentModelManager.createModelReference(uri, 'editor-group-' + this.name), ); } return this.holdDocumentModelRefs.get(key)!; } disposeDocumentRef(uri: URI) { if (uri.scheme === 'diff') { const query = uri.getParsedQuery(); this.doDisposeDocRef(new URI(query.original)); this.doDisposeDocRef(new URI(query.modified)); } else if (uri.scheme === 'mergeEditor') { this.mergeEditor && this.mergeEditor.dispose(); } else { this.doDisposeDocRef(uri); } } protected doDisposeDocRef(uri: URI) { const key = uri.toString(); if (this.holdDocumentModelRefs.has(key)) { this.holdDocumentModelRefs.get(key)!.dispose(); this.holdDocumentModelRefs.delete(key); } } private async openCodeEditor(resource: IResource, options: IResourceOpenOptions) { this.resolveTabChanged(resource, this.currentResource); await this.codeEditorReady.onceReady(async () => { const documentRef = await this.getDocumentModelRef(resource.uri); await this.codeEditor.open(documentRef); if (options.range) { const range = new monaco.Range( options.range.startLineNumber!, options.range.startColumn!, options.range.endLineNumber!, options.range.endColumn!, ); // 这里使用 setTimeout 在下一次事件循环时将编辑器滚动到指定位置 // 原因是在打开新文件的情况下 // setModel 后立即调用 revealRangeInCenter 编辑器无法获取到 viewport 宽高 // 导致无法正确计算滚动位置 this.codeEditor.monacoEditor.setSelection(range); if (options.revealRangeInCenter) { setTimeout(() => { this.codeEditor.monacoEditor.revealRangeInCenter(range, 1); }); } } // 同上 queueMicrotask(() => { if (options.scrollTop) { this.codeEditor.monacoEditor.setScrollTop(options.scrollTop!); } if (options.scrollLeft) { this.codeEditor.monacoEditor.setScrollLeft(options.scrollLeft!); } }); if (options.focus) { const preventScroll = this.getPreventScrollOption(options); this._domNode?.focus({ preventScroll }); // monaco 编辑器的 focus 多了一步检查,由于此时其实对应编辑器的 dom 的 display 为 none (需要等 React 下一次渲染才会改变为 block), // 会引起 document.activeElement !== editor.textArea.domNode,进而会导致focus失败 // 需要等待真正 append 之后再 const disposer = this.eventBus.on(CodeEditorDidVisibleEvent, (e) => { if (e.payload.groupName === this.name && e.payload.type === EditorOpenType.code) { disposer.dispose(); // 此处必须多做一些检查以免不必要的 focus if (this.disposed) { return; } if (this !== this.workbenchEditorService.currentEditorGroup) { return; } if (this.currentEditor === this.codeEditor && this.codeEditor.currentUri?.isEqual(resource.uri)) { try { this.codeEditor.focus(); } catch (e) { // noop } } } }); } // 可能在diff Editor中修改导致为脏 if (documentRef.instance!.dirty) { this.pinPreviewed(resource.uri); } }); } private async openDiffEditor(resource: IResource, options: IResourceOpenOptions) { if (!this.diffEditor) { await this.diffEditorDomReady.onceReady(() => { const container = document.createElement('div'); this._diffEditorDomNode?.appendChild(container); this.createDiffEditor(container); }); } const diffResource = resource as IDiffResource; const [original, modified] = await Promise.all([ this.getDocumentModelRef(diffResource.metadata!.original), this.getDocumentModelRef(diffResource.metadata!.modified), ]); await this.diffEditorReady.onceReady(async () => { if (!original || !modified) { return; } await this.diffEditor.compare(original, modified, options, resource.uri); if (options.focus) { const preventScroll = this.getPreventScrollOption(options); this._domNode?.focus({ preventScroll, }); const disposer = this.eventBus.on(CodeEditorDidVisibleEvent, (e) => { if (e.payload.groupName === this.name && e.payload.type === EditorOpenType.diff) { disposer.dispose(); if (this.disposed) { return; } if (this !== this.workbenchEditorService.currentEditorGroup) { return; } if (this.currentEditor === this.diffEditor.modifiedEditor) { try { this.diffEditor.focus(); } catch (e) { // noop } } } }); } }); } private async openMergeEditor(resource: IResource) { const { metadata } = resource as IMergeEditorResource; if (!metadata) { return; } if (!this.mergeEditor) { await this.mergeEditorDomReady.onceReady(() => { const container = document.createElement('div'); this._mergeEditorDomNode?.appendChild(container); this.createMergeEditor(container); }); } const { ancestor, input1, input2, output } = metadata; const input1Data = MergeEditorInputData.from(input1); const input2Data = MergeEditorInputData.from(input2); const [ancestorRef, input1Ref, outputRef, input2Ref] = await Promise.all([ this.getDocumentModelRef(URI.parse(ancestor)), this.getDocumentModelRef(input1Data.uri), this.getDocumentModelRef(URI.parse(output)), this.getDocumentModelRef(input2Data.uri), ]); await this.mergeEditorReady.onceReady(async () => { await this.mergeEditor.open({ ancestor: { uri: URI.parse(metadata.ancestor), textModel: ancestorRef.instance.getMonacoModel(), baseContent: (ancestorRef.instance as EditorDocumentModel).baseContent || '', }, input1: input1Data.setTextModel(input1Ref.instance.getMonacoModel()), input2: input2Data.setTextModel(input2Ref.instance.getMonacoModel()), output: { uri: URI.parse(metadata.output), textModel: outputRef.instance.getMonacoModel(), }, }); }); } private async openCustomEditor(resource: IResource, componentId?: string) { const component = this.editorComponentRegistry.getEditorComponent(componentId as string); const initialProps = this.editorComponentRegistry.getEditorInitialProps(componentId as string); if (!component) { throw new Error('Cannot find Editor Component with id: ' + componentId); } else { this.activateComponentsProps.set(component, initialProps); if (component.renderMode === EditorComponentRenderMode.ONE_PER_RESOURCE) { const openedResources = this.activeComponents.get(component) || []; const index = openedResources.findIndex((r) => r.uri.toString() === resource.uri.toString()); if (index === -1) { openedResources.push(resource); } this.activeComponents.set(component, openedResources); } else if (component.renderMode === EditorComponentRenderMode.ONE_PER_GROUP) { this.activeComponents.set(component, [resource]); } else if (component.renderMode === EditorComponentRenderMode.ONE_PER_WORKBENCH) { const promises: Promise[] = []; this.workbenchEditorService.editorGroups.forEach((g) => { if (g === this) { return; } const r = g.resources.find((r) => r.uri.isEqual(resource.uri)); if (r) { promises.push(g.close(r.uri)); } }); await Promise.all(promises).catch(getDebugLogger().error); this.activeComponents.set(component, [resource]); } } // 打开非编辑器的component时需要手动触发 this._onCurrentEditorCursorChange.fire({ position: null, selectionLength: 0, }); } private async displayResourceComponent(resource: IResource, options: IResourceOpenOptions) { if (options.revealRangeInCenter === undefined) { options.revealRangeInCenter = true; } const _resource = resource; const result = await this.resolveOpenType(resource, options); if (result) { const { activeOpenType, openTypes } = result; this.availableOpenTypes = openTypes; if (options.preserveFocus) { options.focus = false; } switch (activeOpenType.type) { case EditorOpenType.code: await this.openCodeEditor(resource, options); break; case EditorOpenType.diff: await this.openDiffEditor(resource, options); break; case EditorOpenType.multiDiff: await this.openMultiDiffEditor(resource, options); break; case EditorOpenType.mergeEditor: await this.openMergeEditor(resource); break; case EditorOpenType.component: await this.openCustomEditor(resource, activeOpenType.componentId); break; default: return; } this.resolveTabChanged(_resource, this.currentResource); this._currentOpenType = activeOpenType; this.notifyBodyChanged(); this.doLayoutEditors(); this.cachedResourcesActiveOpenTypes.set(resource.uri.toString(), activeOpenType); } } private resolveTabChanged(lastResource: IResource, curResource: MaybeNull): void { if (lastResource !== curResource) { // 打开过程中改变了tab throw new EditorTabChangedError(); } } public getLastOpenType(resource: IResource) { return this.cachedResourcesActiveOpenTypes.get(resource.uri.toString()); } private async resolveOpenType( resource: IResource, options: IResourceOpenOptions, ): Promise<{ activeOpenType: IEditorOpenType; openTypes: IEditorOpenType[] } | null> { const openTypes = this.cachedResourcesOpenTypes.get(resource.uri.toString()) || (await this.editorComponentRegistry.resolveEditorComponent(resource)); const editorAssociations = this.preferenceService.get<{ [key in string]: string }>('workbench.editorAssociations'); const activeOpenType = findSuitableOpenType( openTypes, this.cachedResourcesActiveOpenTypes.get(resource.uri.toString()), resource, editorAssociations, options.forceOpenType, ); this.cachedResourcesOpenTypes.set(resource.uri.toString(), openTypes); return { activeOpenType, openTypes }; } public async close( uri: URI, { treatAsNotCurrent, force, }: { treatAsNotCurrent?: boolean; force?: boolean; } = {}, ): Promise { const index = this.resources.findIndex((r) => r.uri.toString() === uri.toString()); if (index !== -1) { const resource = this.resources[index]; if (!force) { if (!(await this.shouldClose(resource))) { return false; } } this.resources.splice(index, 1); this._onDidEditorGroupTabOperation.fire({ type: 'close', resource, index, }); this.eventBus.fire( new EditorGroupCloseEvent({ group: this, resource, }), ); if (this.previewURI && this.previewURI.isEqual(uri)) { this.previewURI = null; } // 优先打开用户打开历史中的uri, // 如果历史中的不可打开,打开去除当前关闭目标uri后相同位置的uri, 如果没有,则一直往前找到第一个可用的uri if (resource === this.currentResource && !treatAsNotCurrent) { let nextUri: URI | undefined; while (this.resourceOpenHistory.length > 0) { if ( this.resources.findIndex((r) => r.uri === this.resourceOpenHistory[this.resourceOpenHistory.length - 1]) !== -1 ) { nextUri = this.resourceOpenHistory.pop(); break; } else { this.resourceOpenHistory.pop(); } } if (nextUri) { this.open(nextUri); } else { let i = index; while (i > 0 && !this.resources[i]) { i--; } if (this.resources[i]) { this.open(this.resources[i].uri); } else { this.backToEmpty(); } } } else { this.notifyTabChanged(); } this.removeResouceFromActiveComponents(resource); this.disposeDocumentRef(uri); } if (this.resources.length === 0) { if (this.grid.parent) { // 当前不是最后一个 editor Group this.dispose(); } this.availableOpenTypes = []; } return true; } private removeResouceFromActiveComponents(resource: IResource) { this.activeComponents.forEach((resources, component) => { const i = resources.indexOf(resource); if (i !== -1) { resources.splice(i, 1); this.eventBus.fire(new ResoucesOfActiveComponentChangedEvent({ component, resources })); } }); } private async shouldClose(resource: IResource): Promise { // TODO: 自定义打开方式如果存在保存能力,也要能阻止关闭 const openedResources = this.workbenchEditorService.editorGroups.map((group) => group.resources); if (!(await this.resourceService.shouldCloseResource(resource, openedResources))) { return false; } else { let count = 0; for (const group of openedResources) { for (const res of group) { if (res.uri.isEqual(resource.uri)) { count++; if (count >= 2) { break; } } } } if (count <= 1) { this.resourceService.disposeResource(resource); } return true; } } private backToEmpty() { const oldOpenType = this._currentOpenType; const oldResource = this._currentResource; this._currentResource = null; this.resetOpenType(); this.notifyTabChanged(); this.notifyBodyChanged(); this._currentOrPreviousFocusedEditor = null; this._onDidEditorFocusChange.fire(); // 关闭最后一个时,应该发送一个 EditorGroupChangeEvent this.eventBus.fire( new EditorGroupChangeEvent({ group: this, newOpenType: this.currentOpenType, newResource: this.currentResource, oldOpenType, oldResource, }), ); } /** * 关闭全部 */ async closeAll(): Promise { for (const resource of this.resources) { if (!(await this.shouldClose(resource))) { return false; } } const closed = this.resources.splice(0, this.resources.length); // reverse, 发送事件需要从后往前 closed.reverse().forEach((resource, index) => { this.clearResourceOnClose(resource); this._onDidEditorGroupTabOperation.fire({ type: 'close', resource, index, }); }); this.activeComponents.clear(); if (this.workbenchEditorService.editorGroups.length > 1) { this.dispose(); } this.previewURI = null; this.backToEmpty(); return true; } /** * 关闭已保存(非dirty) */ async closeSaved() { const saved = this.resources.filter((r) => { const decoration = this.resourceService.getResourceDecoration(r.uri); if (!decoration || !decoration.dirty) { return true; } }); for (const resource of saved) { if (!(await this.shouldClose(resource))) { return; } } for (const resource of saved) { await this.close(resource.uri); } } /** * 关闭向右的tab * @param uri */ async closeToRight(uri: URI) { const index = this.resources.findIndex((r) => r.uri.toString() === uri.toString()); if (index !== -1) { const resourcesToClose = this.resources.slice(index + 1); for (const resource of resourcesToClose) { if (!(await this.shouldClose(resource))) { return; } } this.resources.splice(index + 1); resourcesToClose.reverse().forEach((resource, i) => { this._onDidEditorGroupTabOperation.fire({ type: 'close', resource, index: index + 1 + (resourcesToClose.length - 1 - i), }); }); for (const resource of resourcesToClose) { this.clearResourceOnClose(resource); } this.open(uri); } } clearResourceOnClose(resource: IResource) { this.eventBus.fire( new EditorGroupCloseEvent({ group: this, resource, }), ); this.removeResouceFromActiveComponents(resource); } async closeOthers(uri: URI) { const index = this.resources.findIndex((r) => r.uri.toString() === uri.toString()); if (index !== -1) { const resourcesToClose = this.resources.filter((v, i) => i !== index); for (const resource of resourcesToClose) { if (!(await this.shouldClose(resource))) { return; } } const oldResources = this.resources; this.resources = [this.resources[index]]; resourcesToClose.reverse().forEach((resource) => { this._onDidEditorGroupTabOperation.fire({ type: 'close', resource, index: oldResources.indexOf(resource), }); }); for (const resource of resourcesToClose) { this.clearResourceOnClose(resource); } await this.open(uri); } } /** * current opened resource */ get currentResource(): MaybeNull { return this._currentResource; } get currentOpenType(): MaybeNull { return this._currentOpenType; } async changeOpenType(id: string) { const type = this.availableOpenTypes.find((a) => a.type === id || a.componentId === id); if (!type) { return; } if (!this.currentResource) { return; } if (openTypeSimilar(type, this.currentOpenType!)) { return; } const oldOpenType = this.currentOpenType; await this.displayResourceComponent(this.currentResource!, { forceOpenType: type }); this.eventBus.fire( new EditorGroupChangeEvent({ group: this, newOpenType: this.currentOpenType, newResource: this.currentResource, oldOpenType, oldResource: this.currentResource, }), ); } /** * 拖拽drop方法 */ public async dropUri(uri: URI, position: DragOverPosition, sourceGroup?: EditorGroup, targetResource?: IResource) { if (position !== DragOverPosition.CENTER) { await this.split(getSplitActionFromDragDrop(position), uri, { preview: false, focus: true }); } else { // 扔在本体或者tab上 if (!targetResource) { await this.open(uri, { preview: false, focus: true }); } else { const targetIndex = this.resources.indexOf(targetResource); if (targetIndex === -1) { await this.open(uri, { preview: false, focus: true }); } else { const sourceIndex = this.resources.findIndex((resource) => resource.uri.toString() === uri.toString()); if (sourceIndex === -1) { await this.open(uri, { index: targetIndex, preview: false, }); } else { // just move const sourceResource = this.resources[sourceIndex]; if (sourceIndex > targetIndex) { this.resources.splice(sourceIndex, 1); this.resources.splice(targetIndex, 0, sourceResource); this._onDidEditorGroupTabOperation.fire({ type: 'move', resource: sourceResource, oldIndex: sourceIndex, index: targetIndex, }); await this.open(uri, { preview: false }); } else if (sourceIndex < targetIndex) { this.resources.splice(targetIndex + 1, 0, sourceResource); this.resources.splice(sourceIndex, 1); this._onDidEditorGroupTabOperation.fire({ type: 'move', resource: sourceResource, oldIndex: sourceIndex, index: targetIndex, }); await this.open(uri, { preview: false }); } } } } } if (sourceGroup) { if (sourceGroup !== this) { // 从其他group拖动过来 await sourceGroup.close(uri); } else if (position !== DragOverPosition.CENTER) { // split行为 await this.close(uri); } } } gainFocus() { this.workbenchEditorService.setCurrentGroup(this); } focus() { this.gainFocus(); if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.code) { this.codeEditor.focus(); } if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.diff) { this.diffEditor.focus(); } } dispose() { this.grid?.dispose(); this.workbenchEditorService.removeGroup(this); super.dispose(); this.codeEditor && this.codeEditor.dispose(); this.diffEditor && this.diffEditor.dispose(); this.multiDiffEditor && this.multiDiffEditor.dispose(); this.toDispose.forEach((disposable) => disposable.dispose()); this.eventBus.fire( new EditorGroupDisposeEvent({ group: this, }), ); } getState(): IEditorGroupState { const uris = this.resources.filter(couldRevive).map((r) => r.uri.toString()); return { uris, current: this.currentResource && couldRevive(this.currentResource) ? this.currentResource.uri.toString() : undefined, previewIndex: this.previewURI ? uris.indexOf(this.previewURI.toString()) : -1, }; } isCodeEditorMode() { return !!this.currentOpenType && this.currentOpenType.type === EditorOpenType.code; } isDiffEditorMode() { return !!this.currentOpenType && this.currentOpenType.type === EditorOpenType.diff; } isMultiDiffEditorMode() { return !!this.currentOpenType && this.currentOpenType.type === EditorOpenType.multiDiff; } activeEditorIsDirty() { return this.hasDirty() && this.workbenchEditorService.currentEditorGroup === this; } isComponentMode() { return !!this.currentOpenType && this.currentOpenType.type === EditorOpenType.component; } async restoreState(state: IEditorGroupState) { this._restoringState = true; this.previewURI = state.uris[state.previewIndex] ? new URI(state.uris[state.previewIndex]) : null; for (const uri of state.uris) { await this.doOpen(new URI(uri), { disableNavigate: true, backend: true, preview: false, deletedPolicy: 'skip' }); } let targetUri: URI | undefined; if (state.current) { targetUri = new URI(state.current); } else { if (state.uris.length > 0) { targetUri = new URI(state.uris[state.uris.length - 1]!); } } if (targetUri) { if (!(await this.open(targetUri, { deletedPolicy: 'skip' }))) { if (this.resources[0]) { await this.open(this.resources[0].uri); } } } this._restoringState = false; this.notifyTabChanged(); } async saveAll(includeUntitled?: boolean, reason?: SaveReason) { for (const r of this.resources) { // 不保存无标题文件 if (!includeUntitled && r.uri.scheme === Schemes.untitled) { continue; } await this.saveResource(r, reason); } } async saveResource(resource: IResource, reason: SaveReason = SaveReason.Manual) { // 尝试使用 openType 提供的保存方法保存 if (await this.saveByOpenType(resource, reason)) { return; } // 否则使用 document 进行保存 (如果有) const docRef = this.documentModelManager.getModelReference(resource.uri); if (docRef) { await docRef.instance.save(undefined, reason); docRef.dispose(); } } async saveByOpenType(resource: IResource, reason: SaveReason): Promise { const openType = this.cachedResourcesActiveOpenTypes.get(resource.uri.toString()); if (openType && openType.saveResource) { try { await openType.saveResource(resource, reason); return true; } catch (e) { this.logger.error(e); } } return false; } async saveCurrent(reason: SaveReason = SaveReason.Manual) { const resource = this.currentResource; if (!resource) { return; } if (await this.saveByOpenType(resource, reason)) { return; } if (this.currentEditor) { return this.currentEditor.save(); } } hasDirty(): boolean { for (const r of this.resources) { const docRef = this.documentModelManager.getModelDescription(r.uri); if (docRef) { if (docRef.dirty) { return true; } } } return false; } /** * 计算 dirty 数量 */ calcDirtyCount(countedUris: Set = new Set()): number { let count = 0; for (const r of this.resources) { const docRef = this.documentModelManager.getModelDescription(r.uri, 'calc-dirty-count'); if (countedUris.has(r.uri.toString())) { continue; } countedUris.add(r.uri.toString()); if (docRef) { if (docRef.dirty) { count += 1; } } } return count; } componentUndo() { const currentOpenType = this.currentOpenType; if (currentOpenType?.undo) { currentOpenType.undo(this.currentResource!); } } componentRedo() { const currentOpenType = this.currentOpenType; if (currentOpenType?.redo) { currentOpenType.redo(this.currentResource!); } } /** * 防止作为参数被抛入插件进程时出错 */ toJSON() { return { name: this.name, }; } attachMultiDiffEditorDom(domNode: HTMLElement | null | undefined) { if (!this._multiDiffEditorDomNodeAttached) { this._multiDiffEditorDomNode = domNode; this.multiDiffEditorDomReady.ready(); this._multiDiffEditorDomNodeAttached = true; } } createMultiDiffEditor(dom: HTMLElement) { const editor = this.collectionService.createMultiDiffEditor( dom, {}, { [ServiceNames.CONTEXT_KEY_SERVICE]: this.contextKeyService.contextKeyService }, ); this.multiDiffEditor = editor; } private async openMultiDiffEditor(resource: IResource, options: IResourceOpenOptions) { if (!this.multiDiffEditor) { await this.multiDiffEditorDomReady.onceReady(() => { const container = document.createElement('div'); this._multiDiffEditorDomNode?.appendChild(container); this.createMultiDiffEditor(container); }); } await this.multiDiffEditor.compareMultiple(resource, options); } } function findSuitableOpenType( currentAvailable: IEditorOpenType[], prev: IEditorOpenType | undefined, resource: IResource, editorAssociations: { [key in string]: string } | undefined, forceOpenType?: IEditorOpenType, ) { if (forceOpenType) { return currentAvailable.find((p) => openTypeSimilar(p, forceOpenType)) || currentAvailable[0]; } else if (prev) { return currentAvailable.find((p) => openTypeSimilar(p, prev)) || currentAvailable[0]; } if (editorAssociations) { // 如果配置了 workbench.editorAssociations 且 priority 为 option 的情况下符合规则的默认打开方式行为 const matchAvailableType = currentAvailable.find((p) => { const matchAssKey = Object.keys(editorAssociations).find( (r) => match(r, resource.uri.path.toString().toLowerCase()) || match(r, resource.uri.path.base.toLowerCase()), ); const viewType = matchAssKey && editorAssociations[matchAssKey]; if (!viewType) { return false; } const componentId = `${CUSTOM_EDITOR_SCHEME}-${viewType}`; return p.componentId === componentId; }); if (matchAvailableType) { return matchAvailableType; } } return currentAvailable[0]; } function openTypeSimilar(a: IEditorOpenType, b: IEditorOpenType) { return a.type === b.type && (a.type !== EditorOpenType.component || a.componentId === b.componentId); } export interface IResourceTabOperation { type: 'open' | 'close' | 'move'; resource: IResource; oldIndex?: number; index: number; }