import { Autowired, Injectable } from '@opensumi/di'; import { DisposableCollection, EventType, IPosition, OnEvent, PreferenceService, URI, WithEventBus, addDisposableListener, } from '@opensumi/ide-core-browser'; import { WorkbenchEditorService } from '../../common'; import { EditorGroupChangeEvent, EditorGroupCloseEvent, EditorOpenType, EditorSelectionChangeEvent } from '../types'; const HistoryPositionLineThreshold = 7; const HardMaxStateLength = 200; // 超过200个过后,会缩减至100个, 防止反复缩减 const SoftMaxStateLength = 100; @Injectable() export class EditorHistoryService extends WithEventBus { private static readonly MOUSE_NAVIGATION_SETTING = 'editor.mouseBackForwardToNavigate'; @Autowired(PreferenceService) private preferenceService: PreferenceService; @Autowired(WorkbenchEditorService) private editorService: WorkbenchEditorService; private currentIndex = -1; private stack: EditorHistoryState[] = []; private closedStack: URI[] = []; init() { this.registerMouseNavigationListener(); } private registerMouseNavigationListener() { const disposables = new DisposableCollection(); const handleMouseBackForwardSupport = () => { disposables.dispose(); if (this.preferenceService.get(EditorHistoryService.MOUSE_NAVIGATION_SETTING)) { disposables.push(addDisposableListener(window.document, EventType.MOUSE_DOWN, (e) => this.onMouseDown(e))); } this.disposables.push(disposables); }; this.disposables.push( this.preferenceService.onSpecificPreferenceChange(EditorHistoryService.MOUSE_NAVIGATION_SETTING, () => { if (this.preferenceService.get(EditorHistoryService.MOUSE_NAVIGATION_SETTING)) { handleMouseBackForwardSupport(); } }), ); handleMouseBackForwardSupport(); } private onMouseDown(event: MouseEvent) { // Support to navigate in history when mouse buttons 4/5 are pressed switch (event.button) { case 3: event.stopPropagation(); this.back(); break; case 4: event.stopPropagation(); this.forward(); break; } } @OnEvent(EditorSelectionChangeEvent) onEditorSelectionChangeEvent(e: EditorSelectionChangeEvent) { if (e.payload.selections[0]) { this.onNewState( new EditorHistoryState( e.payload.editorUri, { lineNumber: e.payload.selections[0]!.selectionStartLineNumber, column: e.payload.selections[0]!.selectionStartColumn, }, e.payload.group.index, false, ), ); } } @OnEvent(EditorGroupChangeEvent) onEditorGroupChangeEvent(e: EditorGroupChangeEvent) { if ( e.payload.newOpenType && (e.payload.newOpenType.type === EditorOpenType.code || e.payload.newOpenType.type === EditorOpenType.diff) ) { const selections = e.payload.group.currentEditor!.getSelections(); if (selections && selections.length > 0) { this.onNewState( new EditorHistoryState( e.payload.newResource!.uri, { lineNumber: selections[0].selectionStartLineNumber, column: selections[0]!.selectionStartColumn, }, e.payload.group.index, true, ), ); } } } @OnEvent(EditorGroupCloseEvent) onEditorGroupCloseEvent(e: EditorGroupCloseEvent) { this.pushClosed(e.payload.resource.uri); } onNewState(state: EditorHistoryState) { if (this.currentIndex !== this.stack.length - 1) { if (state.isTabChange && this.currentState.isRelevant(state)) { // return; } if (this.currentState && this.currentState.isEqual(state)) { // 这个状态可能来自 back/forward 被调用产生的行为 // 如果相同,不做任何行为 return; } } const isRelevant = this.currentState && this.currentState.isRelevant(state); this.doPushState(state, isRelevant); } get currentState() { return this.stack[this.currentIndex]; } doPushState(state: EditorHistoryState, isRelevant: boolean) { // 如果和最新的状态关联, 则替换最新的状态 this.stack.splice(this.currentIndex + (isRelevant ? 0 : 1)); this.stack.push(state); if (this.stack.length > HardMaxStateLength) { this.stack.splice(0, this.stack.length - SoftMaxStateLength); } this.currentIndex = this.stack.length - 1; } forward() { if (this.currentIndex < this.stack.length - 1) { this.currentIndex++; this.restoreState(this.currentState); } } back() { if (this.currentIndex > 0) { this.currentIndex--; this.restoreState(this.currentState); } } restoreState(state: EditorHistoryState) { if (!state) { return; } const editorGroup = this.editorService.editorGroups[state.groupIndex] || this.editorService.currentEditorGroup; editorGroup.open(state.uri, { range: { startColumn: state.position.column, startLineNumber: state.position.lineNumber, endColumn: state.position.column, endLineNumber: state.position.lineNumber, }, focus: true, }); } pushClosed(uri: URI) { this.closedStack.push(uri); if (this.closedStack.length > HardMaxStateLength) { this.closedStack.splice(0, this.closedStack.length - SoftMaxStateLength); } } popClosed() { const uri = this.closedStack.pop(); if (uri) { this.editorService.open(uri, { focus: true, }); this.closedStack = this.closedStack.filter((u) => !uri.isEqual(u)); } } } export class EditorHistoryState { constructor( public readonly uri: URI, public readonly position: IPosition, public groupIndex: number, public isTabChange: boolean, ) {} isRelevant(anotherState: EditorHistoryState): boolean { if (this.uri.isEqual(anotherState.uri)) { if ( anotherState.position.lineNumber < this.position.lineNumber + HistoryPositionLineThreshold && anotherState.position.lineNumber > this.position.lineNumber - HistoryPositionLineThreshold ) { return true; } if (this.isTabChange || anotherState.isTabChange) { // 如果是 tabChange 类型,我们认为是相关的,这样防止无意义的 0 line 0 column 状态出现 return true; } } return false; } isEqual(anotherState: EditorHistoryState) { return ( this.uri.isEqual(anotherState.uri) && this.position.lineNumber === anotherState.position.lineNumber && this.position.column === anotherState.position.column ); } }