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 ad1d70446d6..82681ca0327 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 fd5c9ccbb1c..98ce23d7b0e 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 24f25a4e52b..7c8ca731d0f 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 5ca15f5772c..f8ce89f9be1 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 533654d987b..3f5ab6f9526 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