diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index ad1d70446d63891082ff7cad375a6d1fb5c2f9e4..82681ca0327ba56c18d1c18610c9bd68a99336c6 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1014,4 +1014,38 @@ suite('vscode API - workspace', () => { } }); + + test('issue #110141 - TextEdit.setEndOfLine applies an edit and invalidates redo stack even when no change is made', async () => { + const file = await createRandomFile('hello\nworld'); + + const document = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(document); + + // apply edit + { + const we = new vscode.WorkspaceEdit(); + we.insert(file, new vscode.Position(0, 5), '2'); + await vscode.workspace.applyEdit(we); + } + + // check the document + { + assert.equal(document.getText(), 'hello2\nworld'); + assert.equal(document.isDirty, true); + } + + // apply no-op edit + { + const we = new vscode.WorkspaceEdit(); + we.set(file, [vscode.TextEdit.setEndOfLine(vscode.EndOfLine.LF)]); + await vscode.workspace.applyEdit(we); + } + + // undo + { + await vscode.commands.executeCommand('undo'); + assert.equal(document.getText(), 'hello\nworld'); + assert.equal(document.isDirty, false); + } + }); }); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index fd5c9ccbb1cfb22a9e9eb2388d45f60849086b40..98ce23d7b0e86a2c3a4c7832b5d193d67224b698 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -695,6 +695,11 @@ export interface ITextModel { */ getEOL(): string; + /** + * Get the end of line sequence predominantly used in the text buffer. + */ + getEndOfLineSequence(): EndOfLineSequence; + /** * Get the minimum legal column for line at `lineNumber` */ diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 24f25a4e52bea3b678d2da36b2e161d83c9b71f9..7c8ca731d0f76266c095ab209ab07712b69f61bf 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -830,6 +830,15 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.getEOL(); } + public getEndOfLineSequence(): model.EndOfLineSequence { + this._assertNotDisposed(); + return ( + this._buffer.getEOL() === '\n' + ? model.EndOfLineSequence.LF + : model.EndOfLineSequence.CRLF + ); + } + public getLineMinColumn(lineNumber: number): number { this._assertNotDisposed(); return 1; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5ca15f5772cb72c800edef18417c31a658d74127..f8ce89f9be1700a3fddd049c3f8e603a9cfe6cab 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1685,6 +1685,10 @@ declare namespace monaco.editor { * @return EOL char sequence (e.g.: '\n' or '\r\n'). */ getEOL(): string; + /** + * Get the end of line sequence predominantly used in the text buffer. + */ + getEndOfLineSequence(): EndOfLineSequence; /** * Get the minimum legal column for line at `lineNumber` */ diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 533654d987b9a14f1ec8e075cc5bd08086209840..3f5ab6f952657cffa8a49fbc890ae5a471689d49 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -39,6 +39,18 @@ class ModelEditTask implements IDisposable { this._modelReference.dispose(); } + isNoOp() { + if (this._edits.length > 0) { + // contains textual edits + return false; + } + if (this._newEol !== undefined && this._newEol !== this.model.getEndOfLineSequence()) { + // contains an eol change that is a real change + return false; + } + return true; + } + addEdit(resourceEdit: ResourceTextEdit): void { this._expectedModelVersionId = resourceEdit.versionId; const { textEdit } = resourceEdit; @@ -219,10 +231,12 @@ export class BulkTextEdits { if (tasks.length === 1) { // This edit touches a single model => keep things simple const task = tasks[0]; - const singleModelEditStackElement = new SingleModelEditStackElement(task.model, task.getBeforeCursorState()); - this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup); - task.apply(); - singleModelEditStackElement.close(); + if (!task.isNoOp()) { + const singleModelEditStackElement = new SingleModelEditStackElement(task.model, task.getBeforeCursorState()); + this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup); + task.apply(); + singleModelEditStackElement.close(); + } this._progress.report(undefined); } else { // prepare multi model undo element