Skip to content

Commit

Permalink
refactor find widget to support multiple selection find/replace
Browse files Browse the repository at this point in the history
  • Loading branch information
jodyheavener committed Jun 1, 2020
1 parent fcd551f commit b9efdab
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 96 deletions.
4 changes: 2 additions & 2 deletions src/vs/editor/common/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,15 +800,15 @@ export interface ITextModel {
/**
* Search the model.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
* @param searchScope Limit the searching to only search inside this range.
* @param searchScope Limit the searching to only search inside these ranges.
* @param isRegex Used to indicate that `searchString` is a regular expression.
* @param matchCase Force the matching to match lower/upper case exactly.
* @param wordSeparators Force the matching to match entire words only. Pass null otherwise.
* @param captureMatches The result will contain the captured groups.
* @param limitResultCount Limit the number of results
* @return The ranges where the matches are. It is empty if no matches have been found.
*/
findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
findMatches(searchString: string, searchScope: IRange | IRange[], isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount?: number): FindMatch[];
/**
* Search the model for the next match. Loops to the beginning of the model if needed.
* @param searchString The string used to search. If it is a regular expression, set `isRegex` to true.
Expand Down
26 changes: 19 additions & 7 deletions src/vs/editor/common/model/textModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,13 +1112,23 @@ export class TextModel extends Disposable implements model.ITextModel {
public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] {
this._assertNotDisposed();

let searchRange: Range;
if (Range.isIRange(rawSearchScope)) {
searchRange = this.validateRange(rawSearchScope);
} else {
searchRange = this.getFullModelRange();
let searchRanges: Range[] | null = null;

if (rawSearchScope !== null) {
if (!Array.isArray(rawSearchScope)) {
rawSearchScope = [rawSearchScope];
}

if (rawSearchScope.every((searchScope: Range) => Range.isIRange(searchScope))) {
searchRanges = rawSearchScope.map((searchScope: Range) => this.validateRange(searchScope));
}
}

if (searchRanges === null) {
searchRanges = [this.getFullModelRange()];
}

let matchMapper: (value: Range, index: number, array: Range[]) => model.FindMatch[];
if (!isRegex && searchString.indexOf('\n') < 0) {
// not regex, not multi line
const searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators);
Expand All @@ -1128,10 +1138,12 @@ export class TextModel extends Disposable implements model.ITextModel {
return [];
}

return this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
matchMapper = (searchRange: Range) => this.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount);
} else {
matchMapper = (searchRange: Range) => TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount);
}

return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wordSeparators), searchRange, captureMatches, limitResultCount);
return searchRanges.map(matchMapper).reduce((arr, matches: model.FindMatch[]) => arr.concat(matches), []);
}

public findNextMatch(searchString: string, rawSearchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean): model.FindMatch | null {
Expand Down
28 changes: 19 additions & 9 deletions src/vs/editor/contrib/find/findController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,22 @@ export class CommonFindController extends Disposable implements IEditorContribut
this._state.change({ searchScope: null }, true);
} else {
if (this._editor.hasModel()) {
let selection = this._editor.getSelection();
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(selection.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(selection.endLineNumber - 1));
}
if (!selection.isEmpty()) {
this._state.change({ searchScope: selection }, true);
let selections = this._editor.getSelections();
selections.map(selection => {
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
selection = selection.setEndPosition(
selection.endLineNumber - 1,
this._editor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
);
}
if (!selection.isEmpty()) {
return selection;
}
return null;
}).filter(element => !!element);

if (selections.length) {
this._state.change({ searchScope: selections }, true);
}
}
}
Expand Down Expand Up @@ -293,9 +303,9 @@ export class CommonFindController extends Disposable implements IEditorContribut
}

if (opts.updateSearchScope) {
let currentSelection = this._editor.getSelection();
if (!currentSelection.isEmpty()) {
stateChanges.searchScope = currentSelection;
let currentSelections = this._editor.getSelections();
if (currentSelections.some(selection => !selection.isEmpty())) {
stateChanges.searchScope = currentSelections;
}
}

Expand Down
41 changes: 27 additions & 14 deletions src/vs/editor/contrib/find/findDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class FindDecorations implements IDisposable {
private readonly _editor: IActiveCodeEditor;
private _decorations: string[];
private _overviewRulerApproximateDecorations: string[];
private _findScopeDecorationId: string | null;
private _findScopeDecorationIds: string[];
private _rangeHighlightDecorationId: string | null;
private _highlightedDecorationId: string | null;
private _startPosition: Position;
Expand All @@ -26,7 +26,7 @@ export class FindDecorations implements IDisposable {
this._editor = editor;
this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
this._startPosition = this._editor.getPosition();
Expand All @@ -37,15 +37,15 @@ export class FindDecorations implements IDisposable {

this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
}

public reset(): void {
this._decorations = [];
this._overviewRulerApproximateDecorations = [];
this._findScopeDecorationId = null;
this._findScopeDecorationIds = [];
this._rangeHighlightDecorationId = null;
this._highlightedDecorationId = null;
}
Expand All @@ -54,9 +54,22 @@ export class FindDecorations implements IDisposable {
return this._decorations.length;
}

/** @deprecated use getFindScopes to support multiple selections */
public getFindScope(): Range | null {
if (this._findScopeDecorationId) {
return this._editor.getModel().getDecorationRange(this._findScopeDecorationId);
if (this._findScopeDecorationIds[0]) {
return this._editor.getModel().getDecorationRange(this._findScopeDecorationIds[0]);
}
return null;
}

public getFindScopes(): Range[] | null {
if (this._findScopeDecorationIds.length) {
const scopes = this._findScopeDecorationIds.map(findScopeDecorationId =>
this._editor.getModel().getDecorationRange(findScopeDecorationId)
).filter(element => !!element);
if (scopes.length) {
return scopes as Range[];
}
}
return null;
}
Expand Down Expand Up @@ -133,7 +146,7 @@ export class FindDecorations implements IDisposable {
return matchPosition;
}

public set(findMatches: FindMatch[], findScope: Range | null): void {
public set(findMatches: FindMatch[], findScopes: Range[] | null): void {
this._editor.changeDecorations((accessor) => {

let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION;
Expand Down Expand Up @@ -195,12 +208,12 @@ export class FindDecorations implements IDisposable {
}

// Find scope
if (this._findScopeDecorationId) {
accessor.removeDecoration(this._findScopeDecorationId);
this._findScopeDecorationId = null;
if (this._findScopeDecorationIds.length) {
this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId));
this._findScopeDecorationIds = [];
}
if (findScope) {
this._findScopeDecorationId = accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION);
if (findScopes?.length) {
this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION));
}
});
}
Expand Down Expand Up @@ -253,8 +266,8 @@ export class FindDecorations implements IDisposable {
let result: string[] = [];
result = result.concat(this._decorations);
result = result.concat(this._overviewRulerApproximateDecorations);
if (this._findScopeDecorationId) {
result.push(this._findScopeDecorationId);
if (this._findScopeDecorationIds.length) {
result.push(...this._findScopeDecorationIds);
}
if (this._rangeHighlightDecorationId) {
result.push(this._rangeHighlightDecorationId);
Expand Down
61 changes: 37 additions & 24 deletions src/vs/editor/contrib/find/findModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,26 +168,36 @@ export class FindModelBoundToEditorModel {
return model.getFullModelRange();
}

private research(moveCursor: boolean, newFindScope?: Range | null): void {
let findScope: Range | null = null;
private research(moveCursor: boolean, newFindScope?: Range | Range[] | null): void {
let findScopes: Range[] | null = null;
if (typeof newFindScope !== 'undefined') {
findScope = newFindScope;
} else {
findScope = this._decorations.getFindScope();
}
if (findScope !== null) {
if (findScope.startLineNumber !== findScope.endLineNumber) {
if (findScope.endColumn === 1) {
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber - 1, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber - 1));
if (newFindScope !== null) {
if (!Array.isArray(newFindScope)) {
findScopes = [newFindScope as Range];
} else {
// multiline find scope => expand to line starts / ends
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber));
findScopes = newFindScope;
}
}
} else {
findScopes = this._decorations.getFindScopes();
}
if (findScopes !== null) {
findScopes = findScopes.map(findScope => {
if (findScope.startLineNumber !== findScope.endLineNumber) {
let endLineNumber = findScope.endLineNumber;

let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT);
this._decorations.set(findMatches, findScope);
if (findScope.endColumn === 1) {
endLineNumber = endLineNumber - 1;
}

return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
}
return findScope;
});
}

let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
this._decorations.set(findMatches, findScopes);

this._state.changeMatchInfo(
this._decorations.getCurrentMatchesPosition(this._editor.getSelection()),
Expand Down Expand Up @@ -443,23 +453,26 @@ export class FindModelBoundToEditorModel {
}
}

private _findMatches(findScope: Range | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
private _findMatches(findScopes: Range[] | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
const searchRanges = (findScopes as [] || [null]).map((scope: Range | null) =>
FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope)
);

return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
}

public replaceAll(): void {
if (!this._hasMatches()) {
return;
}

const findScope = this._decorations.getFindScope();
const findScopes = this._decorations.getFindScopes();

if (findScope === null && this._state.matchesCount >= MATCHES_LIMIT) {
if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
// Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
this._largeReplaceAll();
} else {
this._regularReplaceAll(findScope);
this._regularReplaceAll(findScopes);
}

this.research(false);
Expand Down Expand Up @@ -504,10 +517,10 @@ export class FindModelBoundToEditorModel {
this._executeEditorCommand('replaceAll', command);
}

private _regularReplaceAll(findScope: Range | null): void {
private _regularReplaceAll(findScopes: Range[] | null): void {
const replacePattern = this._getReplacePattern();
// Get all the ranges (even more than the highlighted ones)
let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER);
let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, Constants.MAX_SAFE_SMALL_INTEGER);

let replaceStrings: string[] = [];
for (let i = 0, len = matches.length; i < len; i++) {
Expand All @@ -523,10 +536,10 @@ export class FindModelBoundToEditorModel {
return;
}

let findScope = this._decorations.getFindScope();
let findScopes = this._decorations.getFindScopes();

// Get all the ranges (even more than the highlighted ones)
let matches = this._findMatches(findScope, false, Constants.MAX_SAFE_SMALL_INTEGER);
let matches = this._findMatches(findScopes, false, Constants.MAX_SAFE_SMALL_INTEGER);
let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));

// If one of the ranges is the editor selection, then maintain it as primary
Expand Down
12 changes: 8 additions & 4 deletions src/vs/editor/contrib/find/findState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface INewFindReplaceState {
matchCaseOverride?: FindOptionOverride;
preserveCase?: boolean;
preserveCaseOverride?: FindOptionOverride;
searchScope?: Range | null;
searchScope?: Range[] | null;
loop?: boolean;
}

Expand All @@ -73,7 +73,7 @@ export class FindReplaceState extends Disposable {
private _matchCaseOverride: FindOptionOverride;
private _preserveCase: boolean;
private _preserveCaseOverride: FindOptionOverride;
private _searchScope: Range | null;
private _searchScope: Range[] | null;
private _matchesPosition: number;
private _matchesCount: number;
private _currentMatch: Range | null;
Expand All @@ -94,7 +94,7 @@ export class FindReplaceState extends Disposable {
public get actualMatchCase(): boolean { return this._matchCase; }
public get actualPreserveCase(): boolean { return this._preserveCase; }

public get searchScope(): Range | null { return this._searchScope; }
public get searchScope(): Range[] | null { return this._searchScope; }
public get matchesPosition(): number { return this._matchesPosition; }
public get matchesCount(): number { return this._matchesCount; }
public get currentMatch(): Range | null { return this._currentMatch; }
Expand Down Expand Up @@ -238,7 +238,11 @@ export class FindReplaceState extends Disposable {
this._preserveCase = newState.preserveCase;
}
if (typeof newState.searchScope !== 'undefined') {
if (!Range.equalsRange(this._searchScope, newState.searchScope)) {
if (!newState.searchScope?.every((newSearchScope) => {
return this._searchScope?.some(existingSearchScope => {
return !Range.equalsRange(existingSearchScope, newSearchScope);
});
})) {
this._searchScope = newState.searchScope;
changeEvent.searchScope = true;
somethingChanged = true;
Expand Down
Loading

0 comments on commit b9efdab

Please sign in to comment.