Skip to content

Commit 4e06e1f

Browse files
authored
Fix incorrect rendering when file uses both spaces and tabs for indentation (microsoft#261055)
fixes microsoft/vscode-copilot-issues#163
1 parent 4ef82c3 commit 4e06e1f

File tree

2 files changed

+46
-5
lines changed

2 files changed

+46
-5
lines changed

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class InlineEditsView extends Disposable {
9090
}
9191

9292
if (state.kind === InlineCompletionViewKind.SideBySide) {
93-
const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange);
93+
const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange, textModel.getOptions().tabSize);
9494
newText = indentationAdjustmentEdit.applyToString(newText);
9595

9696
mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit);

src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { findFirstMin } from '../../../../../../../base/common/arraysFind.js';
1010
import { DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js';
1111
import { derived, derivedObservableWithCache, derivedOpts, IObservable, IReader, observableValue, transaction } from '../../../../../../../base/common/observable.js';
1212
import { OS } from '../../../../../../../base/common/platform.js';
13-
import { getIndentationLength, splitLines } from '../../../../../../../base/common/strings.js';
13+
import { splitLines } from '../../../../../../../base/common/strings.js';
1414
import { URI } from '../../../../../../../base/common/uri.js';
1515
import { MenuEntryActionViewItem } from '../../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
1616
import { ICodeEditor } from '../../../../../../browser/editorBrowser.js';
@@ -26,6 +26,8 @@ import { TextReplacement, TextEdit } from '../../../../../../common/core/edits/t
2626
import { RangeMapping } from '../../../../../../common/diff/rangeMapping.js';
2727
import { ITextModel } from '../../../../../../common/model.js';
2828
import { indentOfLine } from '../../../../../../common/model/textModel.js';
29+
import { CharCode } from '../../../../../../../base/common/charCode.js';
30+
import { BugIndicatingError } from '../../../../../../../base/common/errors.js';
2931

3032
export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number {
3133
editor.layoutInfo.read(reader);
@@ -183,12 +185,51 @@ function offsetRangeToRange(columnOffsetRange: OffsetRange, startPos: Position):
183185
);
184186
}
185187

186-
export function createReindentEdit(text: string, range: LineRange): TextEdit {
188+
/**
189+
* Calculates the indentation size (in spaces) of a given line,
190+
* interpreting tabs as the specified tab size.
191+
*/
192+
function getIndentationSize(line: string, tabSize: number): number {
193+
let currentSize = 0;
194+
loop: for (let i = 0, len = line.length; i < len; i++) {
195+
switch (line.charCodeAt(i)) {
196+
case CharCode.Tab: currentSize += tabSize; break;
197+
case CharCode.Space: currentSize++; break;
198+
default: break loop;
199+
}
200+
}
201+
// if currentSize % tabSize !== 0,
202+
// then there are spaces which are not part of the indentation
203+
return currentSize - (currentSize % tabSize);
204+
}
205+
206+
/**
207+
* Calculates the number of characters at the start of a line that correspond to a given indentation size,
208+
* taking into account both tabs and spaces.
209+
*/
210+
function indentSizeToIndentLength(line: string, indentSize: number, tabSize: number): number {
211+
let remainingSize = indentSize - (indentSize % tabSize);
212+
let i = 0;
213+
for (; i < line.length; i++) {
214+
if (remainingSize === 0) {
215+
break;
216+
}
217+
switch (line.charCodeAt(i)) {
218+
case CharCode.Tab: remainingSize -= tabSize; break;
219+
case CharCode.Space: remainingSize--; break;
220+
default: throw new BugIndicatingError('Unexpected character found while calculating indent length');
221+
}
222+
}
223+
return i;
224+
}
225+
226+
export function createReindentEdit(text: string, range: LineRange, tabSize: number): TextEdit {
187227
const newLines = splitLines(text);
188228
const edits: TextReplacement[] = [];
189-
const minIndent = findFirstMin(range.mapToLineArray(l => getIndentationLength(newLines[l - 1])), numberComparator)!;
229+
const minIndentSize = findFirstMin(range.mapToLineArray(l => getIndentationSize(newLines[l - 1], tabSize)), numberComparator)!;
190230
range.forEach(lineNumber => {
191-
edits.push(new TextReplacement(offsetRangeToRange(new OffsetRange(0, minIndent), new Position(lineNumber, 1)), ''));
231+
const indentLength = indentSizeToIndentLength(newLines[lineNumber - 1], minIndentSize, tabSize);
232+
edits.push(new TextReplacement(offsetRangeToRange(new OffsetRange(0, indentLength), new Position(lineNumber, 1)), ''));
192233
});
193234
return new TextEdit(edits);
194235
}

0 commit comments

Comments
 (0)