From 160d6b85905eaba2d207fbec1c0a539f8d7fd139 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 26 Jul 2016 17:54:47 +0200 Subject: [PATCH] fix #6402 --- .../parts/search/browser/searchViewlet.ts | 10 +- .../parts/search/common/searchModel.ts | 183 +++++++++++------- .../search/test/common/searchResult.test.ts | 100 ++++++++++ 3 files changed, 221 insertions(+), 72 deletions(-) diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index b8dd92c2ba7..94ee81f9261 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -84,6 +84,8 @@ export class SearchViewlet extends Viewlet { private inputPatternIncludes: PatternInputWidget; private results: Builder; + private currentSelectedFileMatch: FileMatch; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IEventService private eventService: IEventService, @@ -406,7 +408,7 @@ export class SearchViewlet extends Viewlet { this.toUnbind.push(renderer); this.toUnbind.push(this.tree.addListener2('selection', (event: any) => { - let element: any; + let element: Match; let keyboard = event.payload && event.payload.origin === 'keyboard'; if (keyboard) { element = this.tree.getFocus(); @@ -424,6 +426,11 @@ export class SearchViewlet extends Viewlet { let sideBySide = (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)); let focusEditor = (keyboard && (originalEvent).keyCode === KeyCode.Enter) || doubleClick; + if (this.currentSelectedFileMatch) { + this.currentSelectedFileMatch.setSelectedMatch(null); + } + this.currentSelectedFileMatch = element.parent(); + this.currentSelectedFileMatch.setSelectedMatch(element); this.onFocus(element, !focusEditor, sideBySide, doubleClick); })); }); @@ -892,6 +899,7 @@ export class SearchViewlet extends Viewlet { this.messages.hide(); this.results.show(); this.tree.onVisible(); + this.currentSelectedFileMatch = null; } private onFocus(lineMatch: Match, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index c8e99aaf9c0..52b64c291c9 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -34,8 +34,8 @@ export class Match { constructor(private _parent: FileMatch, text: string, lineNumber: number, offset: number, length: number) { this._lineText = text; - this._id = this._parent.id() + '>' + lineNumber + '>' + offset; this._range = new Range(1 + lineNumber, 1 + offset, 1 + lineNumber, 1 + offset + length); + this._id = this._parent.id() + '>' + lineNumber + '>' + offset + this.getMatchString(); } public id(): string { @@ -56,7 +56,7 @@ export class Match { public preview(): { before: string; inside: string; after: string; } { let before = this._lineText.substring(0, this._range.startColumn - 1), - inside = this._lineText.substring(this._range.startColumn - 1, this._range.endColumn - 1), + inside = this.getMatchString(), after = this._lineText.substring(this._range.endColumn - 1, Math.min(this._range.endColumn + 150, this._lineText.length)); before = strings.lcut(before, 26); @@ -69,26 +69,32 @@ export class Match { } public get replaceString(): string { - return this.parent().parent().searchModel.replacePattern.getReplaceString(this.preview().inside); + return this.parent().parent().searchModel.replacePattern.getReplaceString(this.getMatchString()); + } + + public getMatchString(): string { + return this._lineText.substring(this._range.startColumn - 1, this._range.endColumn - 1); } } export class FileMatch extends Disposable { - private static DecorationOption: IModelDecorationOptions = { - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'findMatch', - overviewRuler: { - color: 'rgba(246, 185, 77, 0.7)', - darkColor: 'rgba(246, 185, 77, 0.7)', - position: OverviewRulerLane.Center - } - }; + private static getDecorationOption(selected: boolean): IModelDecorationOptions { + return { + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: selected ? 'currentFindMatch' : 'findMatch', + overviewRuler: { + color: 'rgba(246, 185, 77, 0.7)', + darkColor: 'rgba(246, 185, 77, 0.7)', + position: OverviewRulerLane.Center + } + }; + } - private _onChange= this._register(new Emitter()); + private _onChange = this._register(new Emitter()); public onChange: Event = this._onChange.event; - private _onDispose= this._register(new Emitter()); + private _onDispose = this._register(new Emitter()); public onDispose: Event = this._onDispose.event; private _resource: URI; @@ -96,16 +102,17 @@ export class FileMatch extends Disposable { private _modelListener: IDisposable; private _matches: SimpleMap; private _removedMatches: ArraySet; + private _selectedMatch: Match; private _updateScheduler: RunOnceScheduler; private _modelDecorations: string[] = []; constructor(private _query: Search.IPatternInfo, private _parent: SearchResult, private rawMatch: Search.IFileMatch, - @IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) { + @IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) { super(); this._resource = this.rawMatch.resource; this._matches = new SimpleMap(); - this._removedMatches= new ArraySet(); + this._removedMatches = new ArraySet(); this._updateScheduler = new RunOnceScheduler(this.updateMatches.bind(this), 250); this.createMatches(); @@ -136,8 +143,8 @@ export class FileMatch extends Disposable { } private bindModel(model: IModel): void { - this._model= model; - this._modelListener= this._model.onDidChangeContent(_ => { + this._model = model; + this._modelListener = this._model.onDidChangeContent(_ => { this._updateScheduler.schedule(); }); this._model.onWillDispose(() => this.onModelWillDispose()); @@ -154,7 +161,7 @@ export class FileMatch extends Disposable { if (this._model) { this._updateScheduler.cancel(); this._model.deltaDecorations(this._modelDecorations, []); - this._model= null; + this._model = null; this._modelListener.dispose(); } } @@ -170,9 +177,12 @@ export class FileMatch extends Disposable { .findMatches(this._query.pattern, this._model.getFullModelRange(), this._query.isRegExp, this._query.isCaseSensitive, this._query.isWordMatch); matches.forEach(range => { - let match= new Match(this, this._model.getLineContent(range.startLineNumber), range.startLineNumber - 1, range.startColumn - 1, range.endColumn - range.startColumn); + let match = new Match(this, this._model.getLineContent(range.startLineNumber), range.startLineNumber - 1, range.startColumn - 1, range.endColumn - range.startColumn); if (!this._removedMatches.contains(match.id())) { this.add(match); + if (this.isMatchSelected(match)) { + this._selectedMatch = match; + } } }); @@ -188,7 +198,7 @@ export class FileMatch extends Disposable { if (this.parent().showHighlights) { this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, this.matches().map(match => { range: match.range(), - options: FileMatch.DecorationOption + options: FileMatch.getDecorationOption(this.isMatchSelected(match)) })); } else { this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, []); @@ -208,18 +218,39 @@ export class FileMatch extends Disposable { } public remove(match: Match): void { - this._matches.delete(match.id()); + this.removeMatch(match); this._removedMatches.set(match.id()); this._onChange.fire(false); } public replace(match: Match): TPromise { return this.replaceService.replace(match).then(() => { - this._matches.delete(match.id()); + this.removeMatch(match); this._onChange.fire(false); }); } + public setSelectedMatch(match: Match) { + if (match) { + if (!this._matches.has(match.id())) { + return; + } + if (this.isMatchSelected(match)) { + return; + } + } + this._selectedMatch = match; + this.updateHighlights(); + } + + public getSelectedMatch(): Match { + return this._selectedMatch; + } + + public isMatchSelected(match: Match): boolean { + return this._selectedMatch && this._selectedMatch.id() === match.id(); + } + public count(): number { return this.matches().length; } @@ -232,18 +263,28 @@ export class FileMatch extends Disposable { return paths.basename(this.resource().fsPath); } - public dispose(): void { - this.unbindModel(); - this._onDispose.fire(); - super.dispose(); - } - public add(match: Match, trigger?: boolean) { this._matches.set(match.id(), match); if (trigger) { this._onChange.fire(true); } } + + private removeMatch(match: Match) { + this._matches.delete(match.id()); + if (this.isMatchSelected(match)) { + this.setSelectedMatch(null); + } else { + this.updateHighlights(); + } + } + + public dispose(): void { + this.setSelectedMatch(null); + this.unbindModel(); + this._onDispose.fire(); + super.dispose(); + } } export interface IChangeEvent { @@ -254,50 +295,50 @@ export interface IChangeEvent { export class SearchResult extends Disposable { - private _onChange= this._register(new Emitter()); + private _onChange = this._register(new Emitter()); public onChange: Event = this._onChange.event; private _fileMatches: SimpleMap; private _unDisposedFileMatches: SimpleMap; - private _query: Search.IPatternInfo= null; + private _query: Search.IPatternInfo = null; private _showHighlights: boolean; - private _replacingAll: boolean= false; + private _replacingAll: boolean = false; constructor(private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @ITelemetryService private telemetryService: ITelemetryService, - @IInstantiationService private instantiationService: IInstantiationService) { + @IInstantiationService private instantiationService: IInstantiationService) { super(); - this._fileMatches= new SimpleMap(); - this._unDisposedFileMatches= new SimpleMap(); + this._fileMatches = new SimpleMap(); + this._unDisposedFileMatches = new SimpleMap(); } public set query(query: Search.IPatternInfo) { - this._query= query; + this._query = query; } public get searchModel(): SearchModel { return this._searchModel; } - public add(raw: Search.IFileMatch[], silent:boolean= false): void { + public add(raw: Search.IFileMatch[], silent: boolean = false): void { let changed: FileMatch[] = []; raw.forEach((rawFileMatch) => { - if (!this._fileMatches.has(rawFileMatch.resource)){ - let fileMatch= this.instantiationService.createInstance(FileMatch, this._query, this, rawFileMatch); + if (!this._fileMatches.has(rawFileMatch.resource)) { + let fileMatch = this.instantiationService.createInstance(FileMatch, this._query, this, rawFileMatch); this.doAdd(fileMatch); changed.push(fileMatch); - let disposable= fileMatch.onChange(() => this.onFileChange(fileMatch)); + let disposable = fileMatch.onChange(() => this.onFileChange(fileMatch)); fileMatch.onDispose(() => disposable.dispose()); } }); if (!silent) { - this._onChange.fire({elements: changed, added: true}); + this._onChange.fire({ elements: changed, added: true }); } } public clear(): void { - let changed: FileMatch[]= this.matches(); + let changed: FileMatch[] = this.matches(); this.disposeMatches(); - this._onChange.fire({elements: changed, removed: true}); + this._onChange.fire({ elements: changed, removed: true }); } public remove(match: FileMatch): void { @@ -311,14 +352,14 @@ export class SearchResult extends Disposable { } public replaceAll(progressRunner: IProgressRunner): TPromise { - this._replacingAll= true; + this._replacingAll = true; let replaceAllTimer = this.telemetryService.timedPublicLog('replaceAll.started'); return this.replaceService.replace(this.matches(), progressRunner).then(() => { replaceAllTimer.stop(); - this._replacingAll= false; + this._replacingAll = false; this.clear(); }, () => { - this._replacingAll= false; + this._replacingAll = false; replaceAllTimer.stop(); }); } @@ -354,19 +395,19 @@ export class SearchResult extends Disposable { } private onFileChange(fileMatch: FileMatch): void { - let added: boolean= false; - let removed: boolean= false; + let added: boolean = false; + let removed: boolean = false; if (!this._fileMatches.has(fileMatch.resource())) { this.doAdd(fileMatch); - added= true; + added = true; } if (fileMatch.count() === 0) { this.doRemove(fileMatch, false, false); - added= false; - removed= true; + added = false; + removed = true; } if (!this._replacingAll) { - this._onChange.fire({elements: [fileMatch], added: added, removed: removed}); + this._onChange.fire({ elements: [fileMatch], added: added, removed: removed }); } } @@ -377,7 +418,7 @@ export class SearchResult extends Disposable { } } - private doRemove(fileMatch: FileMatch, dispose:boolean= true, trigger: boolean= true): void { + private doRemove(fileMatch: FileMatch, dispose: boolean = true, trigger: boolean = true): void { this._fileMatches.delete(fileMatch.resource()); if (dispose) { fileMatch.dispose(); @@ -386,7 +427,7 @@ export class SearchResult extends Disposable { } if (trigger) { - this._onChange.fire({elements: [fileMatch], removed: true}); + this._onChange.fire({ elements: [fileMatch], removed: true }); } } @@ -397,7 +438,7 @@ export class SearchResult extends Disposable { this._unDisposedFileMatches.clear(); } - public dispose():void { + public dispose(): void { this.disposeMatches(); super.dispose(); } @@ -406,27 +447,27 @@ export class SearchResult extends Disposable { export class SearchModel extends Disposable { private _searchResult: SearchResult; - private _searchQuery: ISearchQuery= null; - private _replaceActive: boolean= false; - private _replaceString: string= null; - private _replacePattern: ReplacePattern= null; + private _searchQuery: ISearchQuery = null; + private _replaceActive: boolean = false; + private _replaceString: string = null; + private _replacePattern: ReplacePattern = null; private currentRequest: PPromise; private progressTimer: timer.ITimerEvent; private doneTimer: timer.ITimerEvent; private timerEvent: timer.ITimerEvent; - constructor(@ISearchService private searchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) { + constructor( @ISearchService private searchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) { super(); - this._searchResult= this.instantiationService.createInstance(SearchResult, this); + this._searchResult = this.instantiationService.createInstance(SearchResult, this); } - public isReplaceActive():boolean { + public isReplaceActive(): boolean { return this._replaceActive; } public set replaceActive(replaceActive: boolean) { - this._replaceActive= replaceActive; + this._replaceActive = replaceActive; } public get replacePattern(): ReplacePattern { @@ -438,13 +479,13 @@ export class SearchModel extends Disposable { } public set replaceString(replaceString: string) { - this._replaceString= replaceString; + this._replaceString = replaceString; if (this._searchQuery) { - this._replacePattern= new ReplacePattern(replaceString, this._searchQuery.contentPattern); + this._replacePattern = new ReplacePattern(replaceString, this._searchQuery.contentPattern); } } - public get searchResult():SearchResult { + public get searchResult(): SearchResult { return this._searchResult; } @@ -452,9 +493,9 @@ export class SearchModel extends Disposable { this.cancelSearch(); this.searchResult.clear(); - this._searchQuery= query; - this._searchResult.query= this._searchQuery.contentPattern; - this._replacePattern= new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); + this._searchQuery = query; + this._searchResult.query = this._searchQuery.contentPattern; + this._replacePattern = new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); this.progressTimer = this.telemetryService.timedPublicLog('searchResultsFirstRender'); this.doneTimer = this.telemetryService.timedPublicLog('searchResultsFinished'); @@ -462,8 +503,8 @@ export class SearchModel extends Disposable { this.currentRequest = this.searchService.search(this._searchQuery); this.currentRequest.then(value => this.onSearchCompleted(value), - e => this.onSearchError(e), - p => this.onSearchProgress(p)); + e => this.onSearchError(e), + p => this.onSearchProgress(p)); return this.currentRequest; } diff --git a/src/vs/workbench/parts/search/test/common/searchResult.test.ts b/src/vs/workbench/parts/search/test/common/searchResult.test.ts index 918185da691..39fef7c3019 100644 --- a/src/vs/workbench/parts/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchResult.test.ts @@ -34,6 +34,7 @@ suite('SearchResult', () => { assert.equal(lineMatch.range().endLineNumber, 2); assert.equal(lineMatch.range().startColumn, 1); assert.equal(lineMatch.range().endColumn, 4); + assert.equal('file:///c%3A/folder/file.txt>1>0foo', lineMatch.id()); }); test('Line Match - Remove', function () { @@ -59,6 +60,105 @@ suite('SearchResult', () => { assert.equal(fileMatch.name(), 'file.txt'); }); + test('File Match: Select an existing match', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + + testObject.setSelectedMatch(testObject.matches()[0]); + + assert.equal(testObject.matches()[0], testObject.getSelectedMatch()); + }); + + test('File Match: Select non existing match', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + let target = testObject.matches()[0]; + testObject.remove(target); + + testObject.setSelectedMatch(target); + + assert.equal(undefined, testObject.getSelectedMatch()); + }); + + test('File Match: isSelected return true for selected match', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + let target = testObject.matches()[0]; + testObject.setSelectedMatch(target); + + assert.ok(testObject.isMatchSelected(target)); + }); + + test('File Match: isSelected return false for un-selected match', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + + testObject.setSelectedMatch(testObject.matches()[0]); + + assert.ok(!testObject.isMatchSelected(testObject.matches()[1])); + }); + + test('File Match: unselect', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + + testObject.setSelectedMatch(testObject.matches()[0]); + testObject.setSelectedMatch(null); + + assert.equal(null, testObject.getSelectedMatch()); + }); + + test('File Match: unselect when not selected', function () { + let testObject = aFileMatch('folder\\file.txt', aSearchResult(), ...[{ + preview: 'foo', + lineNumber: 1, + offsetAndLengths: [[0, 3]] + }, { + preview: 'bar', + lineNumber: 1, + offsetAndLengths: [[5, 3]] + }]); + + testObject.setSelectedMatch(null); + + assert.equal(null, testObject.getSelectedMatch()); + }); + test('Alle Drei Zusammen', function () { let searchResult = instantiationService.createInstance(SearchResult, null); let fileMatch = aFileMatch('far\\boo', searchResult); -- GitLab