forked from microsoft/azuredatastudio
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; | ||
import { URI } from 'vs/base/common/uri'; | ||
|
||
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService'); | ||
|
||
export interface IUndoRedoContext { | ||
replaceCurrentElement(others: IUndoRedoElement[]): void; | ||
} | ||
|
||
export interface IUndoRedoElement { | ||
/** | ||
* None, one or multiple resources that this undo/redo element impacts. | ||
*/ | ||
readonly resources: URI[]; | ||
|
||
/** | ||
* The label of the undo/redo element. | ||
*/ | ||
readonly label: string; | ||
|
||
/** | ||
* Undo. | ||
* Will always be called before `redo`. | ||
* Can be called multiple times. | ||
* e.g. `undo` -> `redo` -> `undo` -> `redo` | ||
*/ | ||
undo(ctx: IUndoRedoContext): void; | ||
|
||
/** | ||
* Redo. | ||
* Will always be called after `undo`. | ||
* Can be called multiple times. | ||
* e.g. `undo` -> `redo` -> `undo` -> `redo` | ||
*/ | ||
redo(ctx: IUndoRedoContext): void; | ||
|
||
/** | ||
* Invalidate the edits concerning `resource`. | ||
* i.e. the undo/redo stack for that particular resource has been destroyed. | ||
*/ | ||
invalidate(resource: URI): boolean; | ||
} | ||
|
||
export interface IUndoRedoService { | ||
_serviceBrand: undefined; | ||
|
||
/** | ||
* Add a new element to the `undo` stack. | ||
* This will destroy the `redo` stack. | ||
*/ | ||
pushElement(element: IUndoRedoElement): void; | ||
|
||
/** | ||
* Get the last pushed element. If the last pushed element has been undone, returns null. | ||
*/ | ||
getLastElement(resource: URI): IUndoRedoElement | null; | ||
|
||
/** | ||
* Remove elements that target `resource`. | ||
*/ | ||
removeElements(resource: URI): void; | ||
|
||
canUndo(resource: URI): boolean; | ||
undo(resource: URI): void; | ||
|
||
redo(resource: URI): void; | ||
canRedo(resource: URI): boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { IUndoRedoService, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; | ||
import { URI } from 'vs/base/common/uri'; | ||
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; | ||
import { onUnexpectedError } from 'vs/base/common/errors'; | ||
|
||
class StackElement { | ||
public readonly actual: IUndoRedoElement; | ||
public readonly label: string; | ||
public readonly resources: URI[]; | ||
public readonly strResources: string[]; | ||
|
||
constructor(actual: IUndoRedoElement) { | ||
this.actual = actual; | ||
this.label = actual.label; | ||
this.resources = actual.resources; | ||
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); | ||
} | ||
|
||
public invalidate(resource: URI): void { | ||
if (this.resources.length > 1) { | ||
this.actual.invalidate(resource); | ||
} | ||
} | ||
} | ||
|
||
class ResourceEditStack { | ||
public resource: URI; | ||
public past: StackElement[]; | ||
public future: StackElement[]; | ||
|
||
constructor(resource: URI) { | ||
this.resource = resource; | ||
this.past = []; | ||
this.future = []; | ||
} | ||
} | ||
|
||
export class UndoRedoService implements IUndoRedoService { | ||
_serviceBrand: undefined; | ||
|
||
private readonly _editStacks: Map<string, ResourceEditStack>; | ||
|
||
constructor() { | ||
this._editStacks = new Map<string, ResourceEditStack>(); | ||
} | ||
|
||
public pushElement(_element: IUndoRedoElement): void { | ||
const element = new StackElement(_element); | ||
for (let i = 0, len = element.resources.length; i < len; i++) { | ||
const resource = element.resources[i]; | ||
const strResource = element.strResources[i]; | ||
|
||
let editStack: ResourceEditStack; | ||
if (this._editStacks.has(strResource)) { | ||
editStack = this._editStacks.get(strResource)!; | ||
} else { | ||
editStack = new ResourceEditStack(resource); | ||
this._editStacks.set(strResource, editStack); | ||
} | ||
|
||
// remove the future | ||
for (const futureElement of editStack.future) { | ||
futureElement.invalidate(resource); | ||
} | ||
editStack.future = []; | ||
editStack.past.push(element); | ||
} | ||
} | ||
|
||
public getLastElement(resource: URI): IUndoRedoElement | null { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
if (editStack.future.length > 0) { | ||
return null; | ||
} | ||
if (editStack.past.length === 0) { | ||
return null; | ||
} | ||
return editStack.past[editStack.past.length - 1].actual; | ||
} | ||
return null; | ||
} | ||
|
||
public removeElements(resource: URI): void { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
for (const pastElement of editStack.past) { | ||
pastElement.invalidate(resource); | ||
} | ||
for (const futureElement of editStack.future) { | ||
futureElement.invalidate(resource); | ||
} | ||
this._editStacks.delete(strResource); | ||
} | ||
} | ||
|
||
public canUndo(resource: URI): boolean { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
return (editStack.past.length > 0); | ||
} | ||
return false; | ||
} | ||
|
||
public undo(resource: URI): void { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (!this._editStacks.has(strResource)) { | ||
return; | ||
} | ||
|
||
const editStack = this._editStacks.get(strResource)!; | ||
if (editStack.past.length === 0) { | ||
return; | ||
} | ||
|
||
const element = editStack.past[editStack.past.length - 1]; | ||
|
||
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; | ||
try { | ||
element.actual.undo({ | ||
replaceCurrentElement: (others: IUndoRedoElement[]): void => { | ||
replaceCurrentElement = others; | ||
} | ||
}); | ||
} catch (e) { | ||
onUnexpectedError(e); | ||
editStack.past.pop(); | ||
editStack.future.push(element); | ||
return; | ||
} | ||
|
||
if (replaceCurrentElement === null) { | ||
// regular case | ||
editStack.past.pop(); | ||
editStack.future.push(element); | ||
return; | ||
} | ||
|
||
const replaceCurrentElementMap = new Map<string, StackElement>(); | ||
for (const _replace of replaceCurrentElement) { | ||
const replace = new StackElement(_replace); | ||
for (const strResource of replace.strResources) { | ||
replaceCurrentElementMap.set(strResource, replace); | ||
} | ||
} | ||
|
||
for (let i = 0, len = element.strResources.length; i < len; i++) { | ||
const strResource = element.strResources[i]; | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
for (let j = editStack.past.length - 1; j >= 0; j--) { | ||
if (editStack.past[j] === element) { | ||
if (replaceCurrentElementMap.has(strResource)) { | ||
editStack.past[j] = replaceCurrentElementMap.get(strResource)!; | ||
} else { | ||
editStack.past.splice(j, 1); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
public canRedo(resource: URI): boolean { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
return (editStack.future.length > 0); | ||
} | ||
return false; | ||
} | ||
|
||
redo(resource: URI): void { | ||
const strResource = uriGetComparisonKey(resource); | ||
if (!this._editStacks.has(strResource)) { | ||
return; | ||
} | ||
|
||
const editStack = this._editStacks.get(strResource)!; | ||
if (editStack.future.length === 0) { | ||
return; | ||
} | ||
|
||
const element = editStack.future[editStack.future.length - 1]; | ||
|
||
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; | ||
try { | ||
element.actual.redo({ | ||
replaceCurrentElement: (others: IUndoRedoElement[]): void => { | ||
replaceCurrentElement = others; | ||
} | ||
}); | ||
} catch (e) { | ||
onUnexpectedError(e); | ||
editStack.future.pop(); | ||
editStack.past.push(element); | ||
return; | ||
} | ||
|
||
if (replaceCurrentElement === null) { | ||
// regular case | ||
editStack.future.pop(); | ||
editStack.past.push(element); | ||
return; | ||
} | ||
|
||
const replaceCurrentElementMap = new Map<string, StackElement>(); | ||
for (const _replace of replaceCurrentElement) { | ||
const replace = new StackElement(_replace); | ||
for (const strResource of replace.strResources) { | ||
replaceCurrentElementMap.set(strResource, replace); | ||
} | ||
} | ||
|
||
for (let i = 0, len = element.strResources.length; i < len; i++) { | ||
const strResource = element.strResources[i]; | ||
if (this._editStacks.has(strResource)) { | ||
const editStack = this._editStacks.get(strResource)!; | ||
for (let j = editStack.future.length - 1; j >= 0; j--) { | ||
if (editStack.future[j] === element) { | ||
if (replaceCurrentElementMap.has(strResource)) { | ||
editStack.future[j] = replaceCurrentElementMap.get(strResource)!; | ||
} else { | ||
editStack.future.splice(j, 1); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |