#8515 Refactor Search models

This commit is contained in:
Sandeep Somavarapu
2016-07-06 16:32:35 +02:00
parent a11c9b2477
commit 7580e6865c
8 changed files with 414 additions and 410 deletions
+8
View File
@@ -48,6 +48,14 @@ export class SimpleMap<K extends Key, T> {
return keys;
}
public values(): T[] {
var values: T[] = [];
for (let key in this.map) {
values.push(this.map[key].value);
}
return values;
}
public entries(): Entry<K, T>[] {
var entries: Entry<K, T>[] = [];
for (let key in this.map) {
@@ -37,8 +37,8 @@ class EditorInputCache {
editorInputPromise= this.createInput(fileMatch);
this.cache.set(fileMatch.resource(), editorInputPromise);
this.refreshInput(fileMatch, text, true);
fileMatch.addListener2('disposed', fileMatch => this.disposeInput(fileMatch));
fileMatch.addListener2('changed', fileMatch => this.refreshInput(fileMatch, this.replaceTextcache.get(fileMatch.resource()), false));
fileMatch.onDispose(() => this.disposeInput(fileMatch));
fileMatch.onChange((modelChange) => this.refreshInput(fileMatch, this.replaceTextcache.get(fileMatch.resource()), modelChange));
}
return editorInputPromise;
}
@@ -131,7 +131,7 @@ export class RemoveAction extends Action {
super('remove', nls.localize('RemoveAction.label', "Remove"), 'action-remove');
}
public run(completely:boolean= true): TPromise<any> {
public run(): TPromise<any> {
if (this.element === this.viewer.getFocus()) {
let nextFocusElement= this.getNextFocusElement();
if (nextFocusElement) {
@@ -148,7 +148,7 @@ export class RemoveAction extends Action {
elementToRefresh= parent;
} else {
let parent: FileMatch= <FileMatch>this.element.parent();
parent.remove(<Match>this.element, completely);
parent.remove(<Match>this.element);
elementToRefresh= parent.count() === 0 ? parent.parent() : parent;
}
@@ -185,8 +185,7 @@ export class ReplaceAllAction extends Action {
public run(): TPromise<any> {
this.telemetryService.publicLog('replaceAll.action.selected');
return this.replaceService.replace([this.fileMatch], this.fileMatch.parent().replaceText).then(() => {
new RemoveAction(this.viewer, this.fileMatch).run();
return this.fileMatch.parent().replace(this.fileMatch, this.fileMatch.parent().searchModel.replaceText).then(() => {
this.viewlet.open(this.fileMatch);
});
}
@@ -207,8 +206,7 @@ export class ReplaceAction extends Action {
public run(): TPromise<any> {
this.telemetryService.publicLog('replace.action.selected');
return this.replaceService.replace(this.element, this.element.parent().parent().replaceText).then(() => {
new RemoveAction(this.viewer, this.element).run(false);
return this.element.parent().replace(this.element, this.element.parent().parent().searchModel.replaceText).then(() => {
this.viewlet.open(this.element);
});
}
@@ -91,19 +91,21 @@ class SearchActionProvider extends ContributableActionProvider {
}
public hasActions(tree: ITree, element: any): boolean {
return element instanceof FileMatch || (tree.getInput().isReplaceActive() || element instanceof Match) || super.hasActions(tree, element);
let input= <SearchResult>tree.getInput();
return element instanceof FileMatch || (input.searchModel.isReplaceActive() || element instanceof Match) || super.hasActions(tree, element);
}
public getActions(tree: ITree, element: any): TPromise<IAction[]> {
return super.getActions(tree, element).then(actions => {
let input= <SearchResult>tree.getInput();
if (element instanceof FileMatch) {
actions.unshift(new RemoveAction(tree, element));
if (tree.getInput().isReplaceActive() && element.count() > 0) {
if (input.searchModel.isReplaceActive() && element.count() > 0) {
actions.unshift(this.instantiationService.createInstance(ReplaceAllAction, tree, element, this.viewlet));
}
}
if (element instanceof Match && !(element instanceof EmptyMatch)) {
if (tree.getInput().isReplaceActive()) {
if (input.searchModel.isReplaceActive()) {
actions.unshift(this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet), new RemoveAction(tree, element));
}
}
@@ -173,12 +175,12 @@ export class SearchRenderer extends ActionsRenderer {
elements.push(strings.escape(preview.before));
let input= <SearchResult>tree.getInput();
let showReplaceText= input.hasReplaceText();
let showReplaceText= input.searchModel.hasReplaceText();
elements.push('</span><span class="' + (showReplaceText ? 'replace ' : '') + 'findInFileMatch">');
elements.push(strings.escape(preview.inside));
if (showReplaceText) {
elements.push('</span><span class="replaceMatch">');
elements.push(strings.escape(input.replaceText));
elements.push(strings.escape(input.searchModel.replaceText));
}
elements.push('</span><span>');
elements.push(strings.escape(preview.after));
@@ -186,7 +188,7 @@ export class SearchRenderer extends ActionsRenderer {
$('a.plain')
.innerHtml(elements.join(strings.empty))
.title((preview.before + (input.isReplaceActive() ? input.replaceText : preview.inside) + preview.after).trim().substr(0, 999))
.title((preview.before + (input.searchModel.isReplaceActive() ? input.searchModel.replaceText : preview.inside) + preview.after).trim().substr(0, 999))
.appendTo(domElement);
}
@@ -212,9 +214,9 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider {
if (element instanceof Match) {
let input= <SearchResult>tree.getInput();
if (input.isReplaceActive()) {
if (input.searchModel.isReplaceActive()) {
let preview = element.preview();
return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + input.replaceText + preview.after);
return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + input.searchModel.replaceText + preview.after);
}
return nls.localize('searchResultAria', "{0}, Search result", element.text());
}
@@ -248,10 +250,11 @@ export class SearchController extends DefaultController {
}
private onDelete(tree: ITree, event: IKeyboardEvent): boolean {
let input= <SearchResult>tree.getInput();
let result = false;
let element = tree.getFocus();
if (element instanceof FileMatch ||
(element instanceof Match && tree.getInput().isReplaceActive() && !(element instanceof EmptyMatch))) {
(element instanceof Match && input.searchModel.isReplaceActive() && !(element instanceof EmptyMatch))) {
new RemoveAction(tree, element).run().done(null, errors.onUnexpectedError);
result = true;
}
@@ -260,9 +263,10 @@ export class SearchController extends DefaultController {
}
private onReplace(tree: ITree, event: IKeyboardEvent): boolean {
let input= <SearchResult>tree.getInput();
let result = false;
let element = tree.getFocus();
if (element instanceof Match && tree.getInput().isReplaceActive() && !(element instanceof EmptyMatch)) {
if (element instanceof Match && input.searchModel.isReplaceActive() && !(element instanceof EmptyMatch)) {
this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet).run().done(null, errors.onUnexpectedError);
result = true;
}
@@ -20,7 +20,6 @@ import strings = require('vs/base/common/strings');
import dom = require('vs/base/browser/dom');
import {IAction, Action} from 'vs/base/common/actions';
import {StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import timer = require('vs/base/common/timer');
import {Dimension, Builder, $} from 'vs/base/browser/builder';
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
import {ITree} from 'vs/base/parts/tree/browser/tree';
@@ -32,11 +31,11 @@ import {IEditorGroupService} from 'vs/workbench/services/group/common/groupServi
import {getOutOfWorkspaceEditorResources} from 'vs/workbench/common/editor';
import {FileChangeType, FileChangesEvent, EventType as FileEventType} from 'vs/platform/files/common/files';
import {Viewlet} from 'vs/workbench/browser/viewlet';
import {Match, EmptyMatch, FileMatch, SearchResult, FileMatchOrMatch} from 'vs/workbench/parts/search/common/searchModel';
import {Match, EmptyMatch, FileMatch, SearchModel, FileMatchOrMatch} from 'vs/workbench/parts/search/common/searchModel';
import {getExcludes, QueryBuilder} from 'vs/workbench/parts/search/common/searchQuery';
import {VIEWLET_ID} from 'vs/workbench/parts/search/common/constants';
import {MessageType, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import {ISearchProgressItem, IFileMatch, ISearchComplete, ISearchQuery, IQueryOptions, ISearchConfiguration} from 'vs/platform/search/common/search';
import {ISearchProgressItem, ISearchComplete, ISearchQuery, IQueryOptions, ISearchConfiguration} from 'vs/platform/search/common/search';
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
@@ -67,10 +66,9 @@ export class SearchViewlet extends Viewlet {
private currentRequest: PPromise<ISearchComplete, ISearchProgressItem>;
private loading: boolean;
private queryBuilder: QueryBuilder;
private viewModel: SearchResult;
private viewModel: SearchModel;
private callOnModelChange: lifecycle.IDisposable[];
private replacingAll:boolean= false;
private viewletVisible: IKeybindingContextKey<boolean>;
private actionRegistry: { [key: string]: Action; };
private tree: ITree;
@@ -128,6 +126,7 @@ export class SearchViewlet extends Viewlet {
public create(parent: Builder): TPromise<void> {
super.create(parent);
this.viewModel= this.instantiationService.createInstance(SearchModel);
let builder: Builder;
this.domNode = parent.div({
'class': 'search-viewlet'
@@ -312,21 +311,21 @@ export class SearchViewlet extends Viewlet {
private refreshInputs(): void {
if (this.viewModel) {
this.viewModel.matches().forEach((fileMatch) => {
this.viewModel.searchResult.matches().forEach((fileMatch) => {
this.replaceService.refreshInput(fileMatch, this.viewModel.replaceText);
});
}
}
private replaceAll(): void {
if (this.viewModel.count() === 0) {
if (this.viewModel.searchResult.count() === 0) {
return;
}
let progressRunner= this.progressService.show(100);
let occurrences= this.viewModel.count();
let fileCount= this.viewModel.fileCount();
let occurrences= this.viewModel.searchResult.count();
let fileCount= this.viewModel.searchResult.fileCount();
let replaceValue= this.searchWidget.getReplaceValue() || '';
let afterReplaceAllMessage= replaceValue ? nls.localize('replaceAll.message', "Replaced {0} occurrences across {1} files with {2}.", occurrences, fileCount, replaceValue)
: nls.localize('removeAll.message', "Removed {0} occurrences across {1} files.", occurrences, fileCount);
@@ -339,20 +338,12 @@ export class SearchViewlet extends Viewlet {
};
if (this.messageService.confirm(confirmation)) {
let replaceAllTimer = this.telemetryService.timedPublicLog('replaceAll.started');
this.replacingAll= true;
this.replaceService.replace(this.viewModel.matches(), replaceValue, progressRunner).then(() => {
replaceAllTimer.stop();
this.replacingAll= false;
setTimeout(() => {
progressRunner.done();
this.showEmptyStage();
this.showMessage(afterReplaceAllMessage);
}, 200);
}, (error) => {
replaceAllTimer.stop();
this.viewModel.searchResult.replaceAll(replaceValue, progressRunner).then(() => {
progressRunner.done();
this.searchWidget.setReplaceAllActionState(false);
this.showMessage(afterReplaceAllMessage);
}, (error) => {
progressRunner.done();
this.replacingAll= false;
errors.isPromiseCanceledError(error);
this.messageService.show(Severity.Error, error);
});
@@ -381,7 +372,9 @@ export class SearchViewlet extends Viewlet {
ariaLabel: nls.localize('treeAriaLabel', "Search Results")
});
this.tree.setInput(this.viewModel.searchResult);
this.toUnbind.push(renderer);
this.toUnbind.push(this.viewModel.searchResult.onChange((element) => this.tree.refresh(element)));
this.toUnbind.push(this.tree.addListener2('selection', (event: any) => {
let element: any;
@@ -444,7 +437,7 @@ export class SearchViewlet extends Viewlet {
// Enable highlights if there are searchresults
if (this.viewModel) {
this.viewModel.toggleHighlights(visible);
this.viewModel.searchResult.toggleHighlights(visible);
}
// Open focused element from results in case the editor area is otherwise empty
@@ -508,7 +501,7 @@ export class SearchViewlet extends Viewlet {
}
public clearSearchResults(): void {
this.disposeModel();
this.viewModel.searchResult.clear();
this.showEmptyStage();
this.searchWidget.clear();
if (this.currentRequest) {
@@ -673,14 +666,6 @@ export class SearchViewlet extends Viewlet {
}
private onQueryTriggered(query: ISearchQuery, excludePattern: string, includePattern: string): void {
if (this.currentRequest) {
this.currentRequest.cancel();
this.currentRequest = null;
}
let progressTimer = this.telemetryService.timedPublicLog('searchResultsFirstRender');
let doneTimer = this.telemetryService.timedPublicLog('searchResultsFinished');
// Progress total is 100%
let progressTotal = 100;
let progressRunner = this.progressService.show(progressTotal);
@@ -688,7 +673,6 @@ export class SearchViewlet extends Viewlet {
this.loading = true;
this.searchWidget.searchInput.clearMessage();
this.disposeModel();
this.showEmptyStage();
let handledMatches: { [id: string]: boolean } = Object.create(null);
@@ -696,29 +680,25 @@ export class SearchViewlet extends Viewlet {
// Auto-expand / collapse based on number of matches:
// - alwaysExpandIfOneResult: expand file results if we have just one file result and less than 50 matches on a file
// - expand file results if we have more than one file result and less than 10 matches on a file
if (this.viewModel) {
let matches = this.viewModel.matches();
matches.forEach((match) => {
if (handledMatches[match.id()]) {
return; // if we once handled a result, do not do it again to keep results stable (the user might have expanded/collapsed meanwhile)
}
let matches = this.viewModel.searchResult.matches();
matches.forEach((match) => {
if (handledMatches[match.id()]) {
return; // if we once handled a result, do not do it again to keep results stable (the user might have expanded/collapsed meanwhile)
}
handledMatches[match.id()] = true;
handledMatches[match.id()] = true;
let length = match.matches().length;
if (length < 10 || (alwaysExpandIfOneResult && matches.length === 1 && length < 50)) {
this.tree.expand(match).done(null, errors.onUnexpectedError);
} else {
this.tree.collapse(match).done(null, errors.onUnexpectedError);
}
});
}
let length = match.matches().length;
if (length < 10 || (alwaysExpandIfOneResult && matches.length === 1 && length < 50)) {
this.tree.expand(match).done(null, errors.onUnexpectedError);
} else {
this.tree.collapse(match).done(null, errors.onUnexpectedError);
}
});
};
let timerEvent = timer.start(timer.Topic.WORKBENCH, 'Search');
let isDone = false;
let onComplete = (completed?: ISearchComplete) => {
timerEvent.stop();
isDone = true;
// Complete up to 100% as needed
@@ -729,23 +709,14 @@ export class SearchViewlet extends Viewlet {
progressRunner.done();
}
// Show the final results
if (!this.viewModel) {
this.viewModel = this.instantiationService.createInstance(SearchResult, query.contentPattern);
if (completed) {
this.viewModel.append(completed.results);
}
}
this.viewModel.replaceText= this.searchWidget.getReplaceValue();
this.tree.refresh().then(() => {
autoExpand(true);
}).done(undefined, errors.onUnexpectedError);
let hasResults = !this.viewModel.isEmpty();
let hasResults = !this.viewModel.searchResult.isEmpty();
this.loading = false;
this.telemetryService.publicLog('searchResultsShown', { count: this.viewModel.count(), fileCount: this.viewModel.fileCount() });
this.actionRegistry['refresh'].enabled = true;
this.actionRegistry['vs.tree.collapse'].enabled = hasResults;
@@ -817,14 +788,13 @@ export class SearchViewlet extends Viewlet {
});
}
} else {
this.viewModel.toggleHighlights(true); // show highlights
this.viewModel.searchResult.toggleHighlights(true); // show highlights
// Indicate as status to ARIA
aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.count(), this.viewModel.fileCount()));
aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(), this.viewModel.searchResult.fileCount()));
}
doneTimer.stop();
this.searchWidget.setReplaceAllActionState(this.viewModel.count() > 0);
this.searchWidget.setReplaceAllActionState(this.viewModel.searchResult.count() > 0);
};
let onError = (e: any) => {
@@ -834,9 +804,6 @@ export class SearchViewlet extends Viewlet {
this.loading = false;
isDone = true;
progressRunner.done();
progressTimer.stop();
doneTimer.stop();
this.messageService.show(2 /* ERROR */, e);
}
};
@@ -844,41 +811,14 @@ export class SearchViewlet extends Viewlet {
let total: number = 0;
let worked: number = 0;
let visibleMatches = 0;
let matches: IFileMatch[] = [];
let onProgress = (p: ISearchProgressItem) => {
// Progress
if (p.total) {
total = p.total;
}
if (p.worked) {
worked = p.worked;
}
// Results
if (p.resource) {
matches.push(p);
// Create view model
if (!this.viewModel) {
this.viewModel = this.instantiationService.createInstance(SearchResult, query.contentPattern);
this.tree.setInput(this.viewModel).then(() => {
autoExpand(false);
this.callOnModelChange.push(this.viewModel.addListener2('changed', (e: any) => {
if (!this.replacingAll) {
this.tree.refresh(e, true);
if (e instanceof FileMatch) {
this.replaceService.refreshInput(e, this.viewModel.replaceText, true);
}
}
}));
}).done(null, errors.onUnexpectedError);
}
this.viewModel.append([p]);
progressTimer.stop();
}
};
// Handle UI updates in an interval to show frequent progress and results
@@ -904,15 +844,15 @@ export class SearchViewlet extends Viewlet {
progressWorked++;
progressRunner.worked(1);
}
// Search result tree update
if (visibleMatches !== matches.length) {
visibleMatches = matches.length;
let count= this.viewModel.searchResult.fileCount();
if (visibleMatches !== count) {
visibleMatches= count;
this.tree.refresh().then(() => {
autoExpand(false);
}).done(null, errors.onUnexpectedError);
}
if (count > 0) {
// since we have results now, enable some actions
if (!this.actionRegistry['vs.tree.collapse'].enabled) {
this.actionRegistry['vs.tree.collapse'].enabled = true;
@@ -922,8 +862,8 @@ export class SearchViewlet extends Viewlet {
this.searchWidget.setReplaceAllActionState(false);
this.replaceService.disposeAllInputs();
this.currentRequest = this.searchService.search(query);
this.currentRequest.then(onComplete, onError, onProgress);
this.currentRequest = this.viewModel.search(query);
this.currentRequest.done(onComplete, onError, onProgress);
}
private showEmptyStage(): void {
@@ -936,7 +876,6 @@ export class SearchViewlet extends Viewlet {
// clean up ui
this.replaceService.disposeAllInputs();
this.messages.hide();
this.tree.setInput(this.instantiationService.createInstance(SearchResult, null)).done(null, errors.onUnexpectedError);
this.results.show();
this.tree.onVisible();
}
@@ -1009,10 +948,10 @@ export class SearchViewlet extends Viewlet {
return;
}
let matches = this.viewModel.matches();
let matches = this.viewModel.searchResult.matches();
for (let i = 0, len = matches.length; i < len; i++) {
if (e.resource.toString() === matches[i].resource().toString()) {
this.viewModel.remove(matches[i]);
this.viewModel.searchResult.remove(matches[i]);
}
}
}
@@ -1022,11 +961,11 @@ export class SearchViewlet extends Viewlet {
return;
}
let matches = this.viewModel.matches();
let matches = this.viewModel.searchResult.matches();
for (let i = 0, len = matches.length; i < len; i++) {
if (e.contains(matches[i].resource(), FileChangeType.DELETED)) {
this.viewModel.remove(matches[i]);
this.viewModel.searchResult.remove(matches[i]);
}
}
}
@@ -1052,17 +991,8 @@ export class SearchViewlet extends Viewlet {
this.inputPatternIncludes.dispose();
this.inputPatternExclusions.dispose();
this.disposeModel();
this.viewModel.dispose();
super.dispose();
}
private disposeModel(): void {
if (this.viewModel) {
this.viewModel.dispose();
this.viewModel = null;
}
this.callOnModelChange = lifecycle.dispose(this.callOnModelChange);
}
}
@@ -2,32 +2,38 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {RunOnceScheduler} from 'vs/base/common/async';
import strings = require('vs/base/common/strings');
import URI from 'vs/base/common/uri';
import * as Set from 'vs/base/common/set';
import * as timer from 'vs/base/common/timer';
import paths = require('vs/base/common/paths');
import lifecycle = require('vs/base/common/lifecycle');
import collections = require('vs/base/common/collections');
import {EventEmitter} from 'vs/base/common/eventEmitter';
import {IModel, ITextModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, IModelDecorationOptions} from 'vs/editor/common/editorCommon';
import {Range} from 'vs/editor/common/core/range';
import {IModelService} from 'vs/editor/common/services/modelService';
import strings = require('vs/base/common/strings');
import errors = require('vs/base/common/errors');
import { RunOnceScheduler } from 'vs/base/common/async';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { TPromise, PPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { SimpleMap } from 'vs/base/common/map';
import { ArraySet } from 'vs/base/common/set';
import Event, { Emitter } from 'vs/base/common/event';
import * as Search from 'vs/platform/search/common/search';
import { ISearchProgressItem, ISearchComplete, ISearchQuery } from 'vs/platform/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Range } from 'vs/editor/common/core/range';
import { IModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, IModelDecorationOptions } from 'vs/editor/common/editorCommon';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ISearchService } from 'vs/platform/search/common/search';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
export class Match {
private _parent: FileMatch;
private _lineText: string;
private _id: string;
private _range: Range;
constructor(parent: FileMatch, text: string, lineNumber: number, offset: number, length: number) {
this._parent = parent;
constructor(private _parent: FileMatch, text: string, lineNumber: number, offset: number, length: number) {
this._lineText = text;
this._id = parent.id() + '>' + lineNumber + '>' + offset;
this._id = this._parent.id() + '>' + lineNumber + '>' + offset;
this._range = new Range(1 + lineNumber, 1 + offset, 1 + lineNumber, 1 + offset + length);
}
@@ -69,75 +75,7 @@ export class EmptyMatch extends Match {
}
}
export class FileMatch extends EventEmitter implements lifecycle.IDisposable {
private _parent: SearchResult;
private _resource: URI;
_removedMatches: Set.ArraySet<string>;
_matches: { [key: string]: Match };
constructor(parent: SearchResult, resource: URI) {
super();
this._resource = resource;
this._parent = parent;
this._matches = Object.create(null);
this._removedMatches= new Set.ArraySet<string>();
}
public dispose(): void {
this.emit('disposed', this);
}
public id(): string {
return this.resource().toString();
}
public parent(): SearchResult {
return this._parent;
}
public add(match: Match): void {
this._matches[match.id()] = match;
this.emit('changed', this);
}
public remove(match: Match, completely:boolean= true): void {
delete this._matches[match.id()];
if (completely) {
this._removedMatches.set(match.id());
}
if (this.count() === 0) {
this.add(new EmptyMatch(this));
}
this.emit('changed', this);
}
public matches(): Match[] {
return collections.values(this._matches);
}
public count(): number {
let result = 0;
for (let key in this._matches) {
if (!(this._matches[key] instanceof EmptyMatch)) {
result += 1;
}
}
return result;
}
public resource(): URI {
return this._resource;
}
public name(): string {
return paths.basename(this.resource().fsPath);
}
}
export type FileMatchOrMatch = FileMatch | Match;
export class LiveFileMatch extends FileMatch implements lifecycle.IDisposable {
export class FileMatch extends Disposable {
private static DecorationOption: IModelDecorationOptions = {
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
@@ -149,105 +87,292 @@ export class LiveFileMatch extends FileMatch implements lifecycle.IDisposable {
}
};
private _onChange= this._register(new Emitter<boolean>());
public onChange: Event<boolean> = this._onChange.event;
private _onDispose= this._register(new Emitter<void>());
public onDispose: Event<void> = this._onDispose.event;
private _resource: URI;
private _model: IModel;
private _query: Search.IPatternInfo;
private _modelListener: IDisposable;
private _matches: SimpleMap<string, Match>;
private _removedMatches: ArraySet<string>;
private _updateScheduler: RunOnceScheduler;
private _modelDecorations: string[] = [];
private _unbind: lifecycle.IDisposable[] = [];
_diskFileMatch: FileMatch;
constructor(parent: SearchResult, resource: URI, query: Search.IPatternInfo, model: IModel, fileMatch: FileMatch) {
super(parent, resource);
constructor(private _query: Search.IPatternInfo, private _parent: SearchResult, private rawMatch: Search.IFileMatch,
@IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) {
super();
this._resource = this.rawMatch.resource;
this._matches = new SimpleMap<string, Match>();
this._removedMatches= new ArraySet<string>();
this._updateScheduler = new RunOnceScheduler(this.updateMatches.bind(this), 250);
this._query = query;
this._model = model;
this._diskFileMatch = fileMatch;
this._removedMatches= fileMatch._removedMatches;
this._updateScheduler = new RunOnceScheduler(this._updateMatches.bind(this), 250);
this._unbind.push(this._model.onDidChangeContent(_ => this._updateScheduler.schedule()));
this._updateMatches();
this.createMatches();
this.registerListeners();
}
public dispose(): void {
this._unbind = lifecycle.dispose(this._unbind);
if (!this._isTextModelDisposed()) {
this._model.deltaDecorations(this._modelDecorations, []);
private createMatches(): void {
let model = this.modelService ? this.modelService.getModel(this._resource) : null;
if (model) {
this.bindModel(model);
this.updateMatches();
} else {
this.rawMatch.lineMatches.forEach((rawLineMatch) => {
rawLineMatch.offsetAndLengths.forEach(offsetAndLength => {
let match = new Match(this, rawLineMatch.preview, rawLineMatch.lineNumber, offsetAndLength[0], offsetAndLength[1]);
this._matches.set(match.id(), match);
});
});
}
super.dispose();
}
private _updateMatches(): void {
private registerListeners(): void {
if (this.modelService) {
this._register(this.modelService.onModelAdded((model: IModel) => {
if (model.uri.toString() === this._resource.toString()) {
this.bindModel(model);
}
}));
}
this._register(this.onChange(() => {
if (this.count() === 0) {
let emptyMatch = new EmptyMatch(this);
this._matches.set(emptyMatch.id(), emptyMatch);
}
}));
}
private bindModel(model: IModel): void {
this._model= model;
this._modelListener= this._model.onDidChangeContent(_ => {
this._updateScheduler.schedule();
});
this._model.onWillDispose(() => this.onModelWillDispose());
this.updateHighlights();
}
private onModelWillDispose(): void {
// Update matches because model might have some dirty changes
this.updateMatches();
this.unbindModel();
}
private unbindModel(): void {
if (this._model) {
this._updateScheduler.cancel();
this._model.deltaDecorations(this._modelDecorations, []);
this._model= null;
this._modelListener.dispose();
}
}
private updateMatches(): void {
// this is called from a timeout and might fire
// after the model has been disposed
if (this._isTextModelDisposed()) {
if (!this._model) {
return;
}
this._matches = Object.create(null);
this._matches = new SimpleMap<string, Match>();
let matches = this._model
.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);
if (!this._removedMatches.contains(match.id())) {
this.add(match);
this._matches.set(match.id(), match);
}
});
if (this.count() === 0) {
this.add(new EmptyMatch(this));
}
this.parent().emit('changed', this);
this._onChange.fire(true);
this.updateHighlights();
}
updateHighlights(): void {
if ((<ITextModel>this._model).isDisposed()) {
public updateHighlights(): void {
if (!this._model) {
return;
}
if (this.parent()._showHighlights) {
if (this.parent().showHighlights) {
this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, this.matches().filter(match => !(match instanceof EmptyMatch)).map(match => <IModelDeltaDecoration>{
range: match.range(),
options: LiveFileMatch.DecorationOption
options: FileMatch.DecorationOption
}));
} else {
this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, []);
}
}
private _isTextModelDisposed(): boolean {
return !this._model || (<ITextModel>this._model).isDisposed();
public id(): string {
return this.resource().toString();
}
public remove(match: Match, completely: boolean= true): void {
super.remove(match, completely);
this._diskFileMatch.remove(match, completely);
public parent(): SearchResult {
return this._parent;
}
public matches(): Match[] {
return this._matches.values();
}
public remove(match: Match): void {
this._matches.delete(match.id());
this._removedMatches.set(match.id());
this._onChange.fire(false);
}
public replace(match: Match, replaceText: string): TPromise<any> {
return this.replaceService.replace(match, replaceText).then(() => {
this._matches.delete(match.id());
this._onChange.fire(false);
});
}
public count(): number {
let result = 0;
this.matches().forEach(element => {
if (!(element instanceof EmptyMatch)) {
result += 1;
}
});
return result;
}
public resource(): URI {
return this._resource;
}
public name(): string {
return paths.basename(this.resource().fsPath);
}
public dispose(): void {
this.unbindModel();
this._onDispose.fire();
super.dispose();
}
}
export class SearchResult extends EventEmitter {
export class SearchResult extends Disposable {
private _modelService: IModelService;
private _query: Search.IPatternInfo;
private _replace: string= null;
private _disposables: lifecycle.IDisposable[] = [];
private _matches: { [key: string]: FileMatch; } = Object.create(null);
private _onChange= this._register(new Emitter<any>());
public onChange: Event<any> = this._onChange.event;
_showHighlights: boolean;
private _fileMatches: SimpleMap<URI, FileMatch>;
private _showHighlights: boolean;
private _replacingAll: boolean= false;
constructor(query: Search.IPatternInfo, @IModelService modelService: IModelService) {
constructor(private _searchModel: SearchModel, @IReplaceService private replaceService: IReplaceService, @ITelemetryService private telemetryService: ITelemetryService,
@IInstantiationService private instantiationService: IInstantiationService) {
super();
this._modelService = modelService;
this._query = query;
this._fileMatches= new SimpleMap<URI, FileMatch>();
}
if (this._query) {
this._modelService.onModelAdded(this._onModelAdded, this, this._disposables);
this._modelService.onModelRemoved(this._onModelRemoved, this, this._disposables);
public get searchModel(): SearchModel {
return this._searchModel;
}
public add(query: Search.IPatternInfo, raw: Search.IFileMatch[], silent:boolean= false): void {
raw.forEach((rawFileMatch) => {
if (!this._fileMatches.has(rawFileMatch.resource)){
let fileMatch= this.instantiationService.createInstance(FileMatch, query, this, rawFileMatch);
this._fileMatches.set(rawFileMatch.resource, fileMatch);
let disposable= fileMatch.onChange(() => this.onFileChange(fileMatch));
fileMatch.onDispose(() => disposable.dispose());
}
});
if (!silent) {
this._onChange.fire(this);
}
}
public clear(): void {
this.matches().forEach((fileMatch: FileMatch) => fileMatch.dispose());
this._fileMatches.clear();
this._onChange.fire(this);
}
public remove(match: FileMatch): void {
this._fileMatches.delete(match.resource());
match.dispose();
this._onChange.fire(this);
}
public replace(match: FileMatch, replaceText: string): TPromise<any> {
return this.replaceService.replace([match], replaceText).then(() => {
this.remove(match);
});
}
public replaceAll(replaceText: string, progressRunner: IProgressRunner): TPromise<any> {
this._replacingAll= true;
let replaceAllTimer = this.telemetryService.timedPublicLog('replaceAll.started');
return this.replaceService.replace(this.matches(), replaceText, progressRunner).then(() => {
replaceAllTimer.stop();
this._replacingAll= false;
this.clear();
}, () => {
this._replacingAll= false;
replaceAllTimer.stop();
});
}
public matches(): FileMatch[] {
return this._fileMatches.values();
}
public isEmpty(): boolean {
return this.fileCount() === 0;
}
public fileCount(): number {
return this._fileMatches.size;
}
public count(): number {
return this.matches().reduce<number>((prev, match) => prev + match.count(), 0);
}
public get showHighlights(): boolean {
return this._showHighlights;
}
public toggleHighlights(value: boolean): void {
if (this._showHighlights === value) {
return;
}
this._showHighlights = value;
this.matches().forEach((fileMatch: FileMatch) => {
fileMatch.updateHighlights();
});
}
private onFileChange(file: FileMatch): void {
if (!this._replacingAll) {
this._onChange.fire(file);
}
}
}
export class SearchModel extends Disposable {
private _searchResult: SearchResult;
private _searchQuery: ISearchQuery= null;
private _replaceText: string= null;
private currentRequest: PPromise<ISearchComplete, ISearchProgressItem>;
private progressTimer: timer.ITimerEvent;
private doneTimer: timer.ITimerEvent;
private timerEvent: timer.ITimerEvent;
constructor(@ISearchService private searchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) {
super();
this._searchResult= this.instantiationService.createInstance(SearchResult, this);
}
/**
* Return true if replace is enabled otherwise false
*/
@@ -261,11 +386,15 @@ export class SearchResult extends EventEmitter {
* Can be empty.
*/
public get replaceText(): string {
return this._replace;
return this._replaceText;
}
public set replaceText(replace: string) {
this._replace= replace;
this._replaceText= replace;
}
public get searchResult():SearchResult {
return this._searchResult;
}
/**
@@ -276,101 +405,59 @@ export class SearchResult extends EventEmitter {
return this.isReplaceActive() && !!this.replaceText;
}
private _onModelAdded(model: IModel): void {
let resource = model.uri,
fileMatch = this._matches[resource.toString()];
public search(query: ISearchQuery, silent: boolean= true): PPromise<ISearchComplete, ISearchProgressItem> {
this.cancelSearch();
this.searchResult.clear();
if (fileMatch) {
let liveMatch = new LiveFileMatch(this, resource, this._query, model, fileMatch);
liveMatch.updateHighlights();
this._matches[resource.toString()] = liveMatch;
this.emit('changed', this);
this._searchQuery= query;
this.progressTimer = this.telemetryService.timedPublicLog('searchResultsFirstRender');
this.doneTimer = this.telemetryService.timedPublicLog('searchResultsFinished');
this.timerEvent = timer.start(timer.Topic.WORKBENCH, 'Search');
this.currentRequest = this.searchService.search(this._searchQuery);
this.currentRequest.then(value => this.onSearchCompleted(value, silent),
e => this.onSearchError(e, silent),
p => this.onSearchProgress(p, silent));
return this.currentRequest;
}
private onSearchCompleted(completed: ISearchComplete, silent: boolean): ISearchComplete {
this.timerEvent.stop();
this._searchResult.add(this._searchQuery.contentPattern, completed.results, silent);
this.telemetryService.publicLog('searchResultsShown', { count: this._searchResult.count(), fileCount: this._searchResult.fileCount() });
this.doneTimer.stop();
return completed;
}
private onSearchError(e: any, silent: boolean): void {
if (errors.isPromiseCanceledError(e)) {
this.onSearchCompleted(null, silent);
} else {
this.progressTimer.stop();
this.doneTimer.stop();
}
}
private _onModelRemoved(model: IModel): void {
let resource = model.uri,
fileMatch = this._matches[resource.toString()];
if (fileMatch instanceof LiveFileMatch) {
this.deferredEmit(() => {
this.remove(fileMatch);
this._matches[resource.toString()] = fileMatch._diskFileMatch;
});
private onSearchProgress(p: ISearchProgressItem, silent: boolean): void {
if (p.resource) {
this._searchResult.add(this._searchQuery.contentPattern, [p], silent);
this.progressTimer.stop();
}
}
public append(raw: Search.IFileMatch[]): void {
raw.forEach((rawFileMatch) => {
let fileMatch = this._getOrAdd(rawFileMatch);
if (fileMatch instanceof LiveFileMatch) {
fileMatch = (<LiveFileMatch>fileMatch)._diskFileMatch;
}
rawFileMatch.lineMatches.forEach((rawLineMatch) => {
rawLineMatch.offsetAndLengths.forEach(offsetAndLength => {
let match = new Match(fileMatch, rawLineMatch.preview, rawLineMatch.lineNumber, offsetAndLength[0], offsetAndLength[1]);
fileMatch.add(match);
});
});
});
}
private _getOrAdd(raw: Search.IFileMatch): FileMatch {
return collections.lookupOrInsert(this._matches, raw.resource.toString(), () => {
let model = this._modelService.getModel(raw.resource),
fileMatch = new FileMatch(this, raw.resource);
if (model && this._query) {
fileMatch = new LiveFileMatch(this, raw.resource, this._query, model, fileMatch);
}
return fileMatch;
});
}
public remove(match: FileMatch): void {
delete this._matches[match.resource().toString()];
match.dispose();
this.emit('changed', this);
}
public matches(): FileMatch[] {
return collections.values(this._matches);
}
public isEmpty(): boolean {
return this.fileCount() === 0;
}
public fileCount(): number {
return Object.keys(this._matches).length;
}
public count(): number {
return this.matches().reduce<number>((prev, match) => prev + match.count(), 0);
}
public toggleHighlights(value: boolean): void {
if (this._showHighlights === value) {
return;
}
this._showHighlights = value;
for (let resource in this._matches) {
let match = this._matches[resource];
if (match instanceof LiveFileMatch) {
match.updateHighlights();
}
public cancelSearch(): void{
if (this.currentRequest) {
this.currentRequest.cancel();
this.currentRequest = null;
}
}
public dispose(): void {
this._disposables = lifecycle.dispose(this._disposables);
lifecycle.dispose(this.matches());
this.cancelSearch();
this.searchResult.dispose();
super.dispose();
}
}
}
export type FileMatchOrMatch = FileMatch | Match;
@@ -8,33 +8,30 @@ import * as assert from 'assert';
import uri from 'vs/base/common/uri';
import {Match, FileMatch, SearchResult} from 'vs/workbench/parts/search/common/searchModel';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IRequestService} from 'vs/platform/request/common/request';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IModelService} from 'vs/editor/common/services/modelService';
import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection';
import {InstantiationService} from 'vs/platform/instantiation/common/instantiationService';
import {SearchSorter, SearchDataSource} from 'vs/workbench/parts/search/browser/searchResultsView';
import {TestContextService} from 'vs/workbench/test/common/servicesTestUtils';
import {IFileMatch, ILineMatch} from 'vs/platform/search/common/search';
function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch {
let rawMatch: IFileMatch= {
resource: uri.file('C:\\' + path),
lineMatches: lineMatches
};
return new FileMatch(null, searchResult, rawMatch, null, null);
}
suite('Search - Viewlet', () => {
let instantiation: IInstantiationService;
setup(() => {
let services = new ServiceCollection();
services.set(IWorkspaceContextService, new TestContextService());
services.set(IRequestService, <any>{
getRequestUrl: () => 'file:///folder/file.txt'
});
services.set(IModelService, <any>{
getModel: () => null
});
instantiation = new InstantiationService(services);
instantiation = new InstantiationService(new ServiceCollection());
});
test('Data Source', function () {
let ds = new SearchDataSource();
let result = instantiation.createInstance(SearchResult, null);
result.append([{
result.add(null, [{
resource: uri.parse('file:///c:/foo'),
lineMatches: [{ lineNumber: 1, preview: 'bar', offsetAndLengths: [[0, 1]] }]
}]);
@@ -53,9 +50,9 @@ suite('Search - Viewlet', () => {
});
test('Sorter', function () {
let fileMatch1 = new FileMatch(null, uri.file('C:\\foo'));
let fileMatch2 = new FileMatch(null, uri.file('C:\\with\\path'));
let fileMatch3 = new FileMatch(null, uri.file('C:\\with\\path\\foo'));
let fileMatch1 = aFileMatch('C:\\foo');
let fileMatch2 = aFileMatch('C:\\with\\path');
let fileMatch3 = aFileMatch('C:\\with\\path\\foo');
let lineMatch1 = new Match(fileMatch1, 'bar', 1, 1, 1);
let lineMatch2 = new Match(fileMatch1, 'bar', 2, 1, 1);
let lineMatch3 = new Match(fileMatch1, 'bar', 2, 1, 1);
@@ -6,49 +6,29 @@
import * as assert from 'assert';
import {Match, FileMatch, SearchResult, EmptyMatch} from 'vs/workbench/parts/search/common/searchModel';
import {Model} from 'vs/editor/common/model/model';
import {Emitter} from 'vs/base/common/event';
import {IModel} from 'vs/editor/common/editorCommon';
import URI from 'vs/base/common/uri';
import {IRequestService} from 'vs/platform/request/common/request';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IModelService} from 'vs/editor/common/services/modelService';
import {ServiceCollection} from 'vs/platform/instantiation/common/serviceCollection';
import {InstantiationService} from 'vs/platform/instantiation/common/instantiationService';
import {TestContextService} from 'vs/workbench/test/common/servicesTestUtils';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IFileMatch} from 'vs/platform/search/common/search';
import {IFileMatch, ILineMatch} from 'vs/platform/search/common/search';
function toUri(path: string): URI {
return URI.file('C:\\' + path);
function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch {
let rawMatch: IFileMatch= {
resource: URI.file('C:\\' + path),
lineMatches: lineMatches
};
return new FileMatch(null, searchResult, rawMatch, null, null);
}
suite('Search - Model', () => {
let instantiation: IInstantiationService;
let oneModel: IModel;
setup(() => {
let emitter = new Emitter<any>();
oneModel = Model.createFromString('line1\nline2\nline3', undefined, undefined, URI.parse('file:///folder/file.txt'));
let services = new ServiceCollection();
services.set(IWorkspaceContextService, new TestContextService());
services.set(IRequestService, <any>{
getRequestUrl: () => 'file:///folder/file.txt'
});
services.set(IModelService, <any>{
getModel: () => oneModel,
onModelAdded: emitter.event
});
instantiation = new InstantiationService(services);
});
teardown(() => {
oneModel.dispose();
instantiation = new InstantiationService(new ServiceCollection());
});
test('Line Match', function () {
let fileMatch = new FileMatch(null, toUri('folder\\file.txt'));
let fileMatch = aFileMatch('folder\\file.txt');
let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3);
assert.equal(lineMatch.text(), 'foo bar');
assert.equal(lineMatch.range().startLineNumber, 2);
@@ -58,24 +38,24 @@ suite('Search - Model', () => {
});
test('Line Match - Remove', function () {
let fileMatch = new FileMatch(null, toUri('folder\\file.txt'));
let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3);
fileMatch.add(lineMatch);
assert.equal(fileMatch.matches().length, 1);
let fileMatch = aFileMatch('folder\\file.txt', null, ...[{
preview: 'foo bar',
lineNumber: 1,
offsetAndLengths: [[0, 3]]
}]);
let lineMatch = fileMatch.matches()[0];
fileMatch.remove(lineMatch);
assert.equal(fileMatch.matches().length, 1);
assert.ok(fileMatch.matches()[0] instanceof EmptyMatch);
});
test('File Match', function () {
let fileMatch = new FileMatch(null, toUri('folder\\file.txt'));
let fileMatch = aFileMatch('folder\\file.txt');
assert.equal(fileMatch.matches(), 0);
assert.equal(fileMatch.resource().toString(), 'file:///c%3A/folder/file.txt');
assert.equal(fileMatch.name(), 'file.txt');
fileMatch = new FileMatch(null, toUri('file.txt'));
fileMatch = aFileMatch('file.txt');
assert.equal(fileMatch.matches(), 0);
assert.equal(fileMatch.resource().toString(), 'file:///c%3A/file.txt');
assert.equal(fileMatch.name(), 'file.txt');
@@ -97,7 +77,7 @@ suite('Search - Model', () => {
}]
});
}
searchResult.append(raw);
searchResult.add(null, raw);
assert.equal(searchResult.isEmpty(), false);
assert.equal(searchResult.matches().length, 10);
@@ -106,7 +86,7 @@ suite('Search - Model', () => {
test('Alle Drei Zusammen', function () {
let searchResult = instantiation.createInstance(SearchResult, null);
let fileMatch = new FileMatch(searchResult, toUri('far\\boo'));
let fileMatch = aFileMatch('far\\boo', searchResult);
let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3);
assert(lineMatch.parent() === fileMatch);