Have the ChangeTracker filter out edits that are no-ops (#38123)

* Filter out edits that are no-ops in 'organize imports'.

* Updated tests for 'organize imports'.

* Always remove no-op changes from the change tracker.

* Add a new `stringContainsAt` helper function to avoid traversing the entire file contents.

* Combine `map`/`filter` sequence into `mapDefined`.

* Fix up documentation.
This commit is contained in:
Daniel Rosenwasser
2020-04-23 12:54:49 -07:00
committed by GitHub
parent 9569e8aaa4
commit d7e437a409
3 changed files with 87 additions and 15 deletions

View File

@@ -887,7 +887,7 @@ namespace ts.textChanges {
namespace changesToText {
export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] {
return group(changes, c => c.sourceFile.path).map(changesInFile => {
return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => {
const sourceFile = changesInFile[0].sourceFile;
// order changes by start position
// If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
@@ -897,9 +897,20 @@ namespace ts.textChanges {
Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () =>
`${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`);
}
const textChanges = normalized.map(c =>
createTextChange(createTextSpanFromRange(c.range), computeNewText(c, sourceFile, newLineCharacter, formatContext, validate)));
return { fileName: sourceFile.fileName, textChanges };
const textChanges = mapDefined(normalized, c => {
const span = createTextSpanFromRange(c.range);
const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate);
// Filter out redundant changes.
if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) {
return undefined;
}
return createTextChange(span, newText);
});
return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined;
});
}

View File

@@ -2827,6 +2827,35 @@ namespace ts {
return symbol.name;
}
/**
* Useful to check whether a string contains another string at a specific index
* without allocating another string or traversing the entire contents of the outer string.
*
* This function is useful in place of either of the following:
*
* ```ts
* // Allocates
* haystack.substr(startIndex, needle.length) === needle
*
* // Full traversal
* haystack.indexOf(needle, startIndex) === startIndex
* ```
*
* @param haystack The string that potentially contains `needle`.
* @param needle The string whose content might sit within `haystack`.
* @param startIndex The index within `haystack` to start searching for `needle`.
*/
export function stringContainsAt(haystack: string, needle: string, startIndex: number) {
const needleLength = needle.length;
if (needleLength + startIndex > haystack.length) {
return false;
}
for (let i = 0; i < needleLength; i++) {
if (needle.charCodeAt(i) !== haystack.charCodeAt(i + startIndex)) return false;
}
return true;
}
export function startsWithUnderscore(name: string): boolean {
return name.charCodeAt(0) === CharacterCodes._;
}