mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-18 18:03:19 +01:00
#8515 Refactor Search models
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user