Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contributable SearchProvider #32549

Merged
merged 7 commits into from
Aug 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/vs/platform/progress/common/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export const emptyProgress: IProgress<any> = Object.freeze({ report() { } });

export class Progress<T> implements IProgress<T> {

private _callback: () => void;
private _callback: (data: T) => void;
private _value: T;

constructor(callback: () => void) {
constructor(callback: (data: T) => void) {
this._callback = callback;
}

Expand All @@ -52,7 +52,7 @@ export class Progress<T> implements IProgress<T> {

report(item: T) {
this._value = item;
this._callback();
this._callback(this._value);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/vs/platform/search/common/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as paths from 'vs/base/common/paths';
import * as glob from 'vs/base/common/glob';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';

export const ID = 'searchService';

Expand All @@ -24,6 +25,11 @@ export interface ISearchService {
search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem>;
extendQuery(query: ISearchQuery): void;
clearCache(cacheKey: string): TPromise<void>;
registerSearchResultProvider(provider: ISearchResultProvider): IDisposable;
}

export interface ISearchResultProvider {
search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem>;
}

export interface IFolderQuery {
Expand Down
5 changes: 4 additions & 1 deletion src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ declare module 'vscode' {

resolveContents(resource: Uri): string | Thenable<string>;
writeContents(resource: Uri, contents: string): void | Thenable<void>;

// -- search
// todo@joh - extract into its own provider?
findFiles(query: string, progress: Progress<Uri>, token?: CancellationToken): Thenable<void>;
}

export namespace workspace {

export function registerFileSystemProvider(authority: string, provider: FileSystemProvider): Disposable;
}

Expand Down
63 changes: 56 additions & 7 deletions src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@

import { isPromiseCanceledError } from 'vs/base/common/errors';
import URI from 'vs/base/common/uri';
import { ISearchService, QueryType, ISearchQuery } from 'vs/platform/search/common/search';
import { ISearchService, QueryType, ISearchQuery, ISearchProgressItem, ISearchComplete } from 'vs/platform/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ICommonCodeEditor, isCommonCodeEditor } from 'vs/editor/common/editorCommon';
import { bulkEdit, IResourceEdit } from 'vs/editor/common/services/bulkEdit';
import { TPromise } from 'vs/base/common/winjs.base';
import { TPromise, PPromise } from 'vs/base/common/winjs.base';
import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IFileService } from 'vs/platform/files/common/files';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { RemoteFileService, IRemoteFileSystemProvider } from 'vs/workbench/services/files/electron-browser/remoteFileService';
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { RemoteFileService } from 'vs/workbench/services/files/electron-browser/remoteFileService';
import { Emitter } from 'vs/base/common/event';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';

Expand Down Expand Up @@ -124,7 +124,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {

// --- EXPERIMENT: workspace provider

private _provider = new Map<number, [IRemoteFileSystemProvider, Emitter<URI>]>();
private _idPool: number = 0;
private readonly _provider = new Map<number, [IDisposable, Emitter<URI>]>();
private readonly _searchSessions = new Map<number, { resolve: (result: ISearchComplete) => void, reject: Function, progress: (item: ISearchProgressItem) => void, matches: URI[] }>();

$registerFileSystemProvider(handle: number, authority: string): void {
if (!(this._fileService instanceof RemoteFileService)) {
Expand All @@ -140,13 +142,60 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
return this._proxy.$storeFile(handle, resource, value);
}
};
this._provider.set(handle, [provider, emitter]);
this._fileService.registerProvider(authority, provider);
const searchProvider = {
search: (query) => {
if (query.type !== QueryType.File) {
return undefined;
}
const session = ++this._idPool;
return new PPromise<any, any>((resolve, reject, progress) => {
this._searchSessions.set(session, { resolve, reject, progress, matches: [] });
this._proxy.$startSearch(handle, session, query.filePattern);
}, () => {
this._proxy.$cancelSearch(handle, session);
});
}
};
const registrations = combinedDisposable([
this._fileService.registerProvider(authority, provider),
this._searchService.registerSearchResultProvider(searchProvider),
]);
this._provider.set(handle, [registrations, emitter]);
}

$unregisterFileSystemProvider(handle: number): void {
if (this._provider.has(handle)) {
dispose(this._provider.get(handle)[0]);
this._provider.delete(handle);
}
}

$onFileSystemChange(handle: number, resource: URI) {
const [, emitter] = this._provider.get(handle);
emitter.fire(resource);
};

$updateSearchSession(session: number, data: URI): void {
if (this._searchSessions.has(session)) {
this._searchSessions.get(session).progress({ resource: data });
this._searchSessions.get(session).matches.push(data);
}
}

$finishSearchSession(session: number, err?: any): void {
if (this._searchSessions.has(session)) {
const { matches, resolve, reject } = this._searchSessions.get(session);
this._searchSessions.delete(session);
if (err) {
reject(err);
} else {
resolve({
limitHit: false,
stats: undefined,
results: matches.map(resource => ({ resource }))
});
}
}
}
}

7 changes: 7 additions & 0 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,12 @@ export interface MainThreadWorkspaceShape extends IDisposable {
$cancelSearch(requestId: number): Thenable<boolean>;
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
$applyWorkspaceEdit(edits: IResourceEdit[]): TPromise<boolean>;

$registerFileSystemProvider(handle: number, authority: string): void;
$unregisterFileSystemProvider(handle): void;
$onFileSystemChange(handle: number, resource: URI): void;
$updateSearchSession(session: number, data): void;
$finishSearchSession(session: number, err?: any): void;
}

export interface MainThreadTaskShape extends IDisposable {
Expand Down Expand Up @@ -439,8 +443,11 @@ export interface ExtHostTreeViewsShape {

export interface ExtHostWorkspaceShape {
$acceptWorkspaceData(workspace: IWorkspaceData): void;

$resolveFile(handle: number, resource: URI): TPromise<string>;
$storeFile(handle: number, resource: URI, content: string): TPromise<any>;
$startSearch(handle: number, session: number, query: string): void;
$cancelSearch(handle: number, session: number): void;
}

export interface ExtHostExtensionServiceShape {
Expand Down
38 changes: 31 additions & 7 deletions src/vs/workbench/api/node/extHostWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { compare } from 'vs/base/common/strings';
import { asWinJsPromise } from 'vs/base/common/async';
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
import { TrieMap } from 'vs/base/common/map';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Progress } from 'vs/platform/progress/common/progress';

class Workspace2 extends Workspace {

Expand Down Expand Up @@ -207,27 +209,49 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {

// --- EXPERIMENT: workspace resolver

private readonly _provider = new Map<number, vscode.FileSystemProvider>();

public registerFileSystemProvider(authority: string, provider: vscode.FileSystemProvider): vscode.Disposable {
private _handlePool = 0;
private readonly _fsProvider = new Map<number, vscode.FileSystemProvider>();
private readonly _searchSession = new Map<number, CancellationTokenSource>();

const handle = this._provider.size;
this._provider.set(handle, provider);
registerFileSystemProvider(authority: string, provider: vscode.FileSystemProvider): vscode.Disposable {
const handle = ++this._handlePool;
this._fsProvider.set(handle, provider);
const reg = provider.onDidChange(e => this._proxy.$onFileSystemChange(handle, <URI>e));
this._proxy.$registerFileSystemProvider(handle, authority);
return new Disposable(() => {
this._provider.delete(handle);
this._fsProvider.delete(handle);
reg.dispose();
});
}

$resolveFile(handle: number, resource: URI): TPromise<string> {
const provider = this._provider.get(handle);
const provider = this._fsProvider.get(handle);
return asWinJsPromise(token => provider.resolveContents(resource));
}

$storeFile(handle: number, resource: URI, content: string): TPromise<any> {
const provider = this._provider.get(handle);
const provider = this._fsProvider.get(handle);
return asWinJsPromise(token => provider.writeContents(resource, content));
}

$startSearch(handle: number, session: number, query: string): void {
const provider = this._fsProvider.get(handle);
const source = new CancellationTokenSource();
const progress = new Progress<any>(chunk => this._proxy.$updateSearchSession(session, chunk));

this._searchSession.set(session, source);
TPromise.wrap(provider.findFiles(query, progress, source.token)).then(() => {
this._proxy.$finishSearchSession(session);
}, err => {
this._proxy.$finishSearchSession(session, err);
});
}

$cancelSearch(handle: number, session: number): void {
if (this._searchSession.has(session)) {
this._searchSession.get(session).cancel();
this._searchSession.delete(session);
}
}
}
92 changes: 62 additions & 30 deletions src/vs/workbench/services/search/node/searchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import scorer = require('vs/base/common/scorer');
import strings = require('vs/base/common/strings');
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, ISearchConfiguration, ISearchService, pathIncludedInQuery } from 'vs/platform/search/common/search';
import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, ISearchConfiguration, ISearchService, pathIncludedInQuery, ISearchResultProvider } from 'vs/platform/search/common/search';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
Expand All @@ -20,11 +20,13 @@ import { IRawSearch, IFolderSearch, ISerializedSearchComplete, ISerializedSearch
import { ISearchChannel, SearchChannelClient } from './searchIpc';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ResourceMap } from 'vs/base/common/map';
import { IDisposable } from 'vs/base/common/lifecycle';

export class SearchService implements ISearchService {
public _serviceBrand: any;

private diskSearch: DiskSearch;
private readonly searchProvider: ISearchResultProvider[] = [];

constructor(
@IModelService private modelService: IModelService,
Expand All @@ -34,6 +36,19 @@ export class SearchService implements ISearchService {
@IConfigurationService private configurationService: IConfigurationService
) {
this.diskSearch = new DiskSearch(!environmentService.isBuilt || environmentService.verbose);
this.registerSearchResultProvider(this.diskSearch);
}

public registerSearchResultProvider(provider: ISearchResultProvider): IDisposable {
this.searchProvider.push(provider);
return {
dispose: () => {
const idx = this.searchProvider.indexOf(provider);
if (idx >= 0) {
this.searchProvider.splice(idx, 1);
}
}
};
}

public extendQuery(query: ISearchQuery): void {
Expand All @@ -59,49 +74,66 @@ export class SearchService implements ISearchService {
}

public search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem> {
let rawSearchQuery: PPromise<void, ISearchProgressItem>;
return new PPromise<ISearchComplete, ISearchProgressItem>((onComplete, onError, onProgress) => {

const searchP = this.diskSearch.search(query);
let combinedPromise: TPromise<void>;

return new PPromise<ISearchComplete, ISearchProgressItem>((onComplete, onError, onProgress) => {

// Get local results from dirty/untitled
const localResults = this.getLocalResults(query);

// Allow caller to register progress callback
process.nextTick(() => localResults.values().filter((res) => !!res).forEach(onProgress));

rawSearchQuery = searchP.then(

// on Complete
(complete) => {
onComplete({
limitHit: complete.limitHit,
results: complete.results.filter((match) => !localResults.has(match.resource)), // dont override local results
stats: complete.stats
});
},

// on Error
(error) => {
onError(error);
const providerPromises = this.searchProvider.map(provider => TPromise.wrap(provider.search(query)).then(e => e,
err => {
// TODO@joh
// single provider fail. fail all?
onError(err);
},

// on Progress
(progress) => {

// Match
progress => {
if (progress.resource) {
// Match
if (!localResults.has(progress.resource)) { // don't override local results
onProgress(progress);
}
} else {
// Progress
onProgress(<IProgress>progress);
}
}
));

// Progress
else {
onProgress(<IProgress>progress);
combinedPromise = TPromise.join(providerPromises).then(values => {

const result: ISearchComplete = {
limitHit: false,
results: [],
stats: undefined
};

// TODO@joh
// sorting, disjunct results
for (const value of values) {
if (!value) {
continue;
}
});
}, () => rawSearchQuery && rawSearchQuery.cancel());
// TODO@joh individual stats/limit
result.stats = value.stats || result.stats;
result.limitHit = value.limitHit || result.limitHit;

for (const match of value.results) {
if (!localResults.has(match.resource)) {
result.results.push(match);
}
}
}

return result;

}).then(onComplete, onError);

}, () => combinedPromise && combinedPromise.cancel());
}

private getLocalResults(query: ISearchQuery): ResourceMap<IFileMatch> {
Expand Down Expand Up @@ -176,7 +208,7 @@ export class SearchService implements ISearchService {
}
}

export class DiskSearch {
export class DiskSearch implements ISearchResultProvider {

private raw: IRawSearchService;

Expand Down Expand Up @@ -286,4 +318,4 @@ export class DiskSearch {
public clearCache(cacheKey: string): TPromise<void> {
return this.raw.clearCache(cacheKey);
}
}
}