diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b6fb23f6ac4..337f7377c26 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -17,9 +17,9 @@ declare module 'vscode' { export interface TextSearchQuery { pattern: string; - isRegExp: boolean; - isCaseSensitive: boolean; - isWordMatch: boolean; + isRegExp?: boolean; + isCaseSensitive?: boolean; + isWordMatch?: boolean; } export interface SearchOptions { @@ -51,8 +51,20 @@ declare module 'vscode' { provideTextSearchResults?(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Thenable; } + export interface FindTextInFilesOptions { + includes?: GlobPattern[]; + excludes?: GlobPattern[]; + maxResults?: number; + useIgnoreFiles?: boolean; + followSymlinks?: boolean; + encoding?: string; + } + export namespace workspace { export function registerSearchProvider(scheme: string, provider: SearchProvider): Disposable; + + // Maybe keep includes and excludes separate to mirror findFiles and make it more natural to pass 'null' to disable excludes + export function findTextInFiles(query: TextSearchQuery, options: FindTextInFilesOptions, callback: (result: TextSearchResult) => void, token?: CancellationToken): Thenable; } //#endregion diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 297e9886d14..371e4abab23 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -5,18 +5,20 @@ 'use strict'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import URI, { UriComponents } from 'vs/base/common/uri'; -import { ISearchService, QueryType, ISearchQuery, IFolderQuery, ISearchConfiguration } from 'vs/platform/search/common/search'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFileMatch, IFolderQuery, IPatternInfo, IQueryOptions, ISearchConfiguration, ISearchQuery, ISearchService, QueryType } from 'vs/platform/search/common/search'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape } from '../node/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -32,7 +34,8 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ITextFileService private readonly _textFileService: ITextFileService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, - @IStatusbarService private readonly _statusbarService: IStatusbarService + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); @@ -97,7 +100,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- search --- - $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable { + $startFileSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable { const workspace = this._contextService.getWorkspace(); if (!workspace.folders.length) { return undefined; @@ -158,6 +161,37 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return search; } + $startTextSearch(pattern: IPatternInfo, options: IQueryOptions, requestId: number): TPromise { + const workspace = this._contextService.getWorkspace(); + const folders = workspace.folders.map(folder => folder.uri); + + const queryBuilder = this._instantiationService.createInstance(QueryBuilder); + const query = queryBuilder.text(pattern, folders, options); + + return new TPromise((resolve, reject) => { + const search = this._searchService.search(query).then( + () => { + delete this._activeSearches[requestId]; + resolve(null); + }, + err => { + delete this._activeSearches[requestId]; + if (!isPromiseCanceledError(err)) { + reject(TPromise.wrapError(err)); + } + + return undefined; + }, + p => { + if (p.lineMatches) { + this._proxy.$handleTextSearchResult(p, requestId); + } + }); + + this._activeSearches[requestId] = search; + }); + } + $cancelSearch(requestId: number): Thenable { const search = this._activeSearches[requestId]; if (search) { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 15853b9ba76..be03ceab953 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -500,6 +500,9 @@ export function createApiFactory( findFiles: (include, exclude, maxResults?, token?) => { return extHostWorkspace.findFiles(typeConverters.GlobPattern.from(include), typeConverters.GlobPattern.from(exclude), maxResults, extension.id, token); }, + findTextInFiles: (query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, token?: vscode.CancellationToken) => { + return extHostWorkspace.findTextInFiles(query, options, callback, extension.id, token); + }, saveAll: (includeUntitled?) => { return extHostWorkspace.saveAll(includeUntitled); }, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 268e78e791d..21850c7ed3a 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -4,52 +4,43 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { createMainContextProxyIdentifier as createMainId, createExtHostContextProxyIdentifier as createExtId, ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; - -import * as vscode from 'vscode'; - -import URI, { UriComponents } from 'vs/base/common/uri'; +import { SerializedError } from 'vs/base/common/errors'; +import { IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; - -import { IMarkerData } from 'vs/platform/markers/common/markers'; -import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; -import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { IProgressOptions, IProgressStep } from 'vs/workbench/services/progress/common/progress'; - -import * as editorCommon from 'vs/editor/common/editorCommon'; -import * as modes from 'vs/editor/common/modes'; - -import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; -import { IConfig, IAdapterExecutable, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; - -import { IQuickPickItem, IPickOptions, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; -import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; -import { EndOfLine, TextEditorLineNumbersStyle, IFileOperationOptions } from 'vs/workbench/api/node/extHostTypes'; - - -import { TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; -import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; - -import { ITreeItem } from 'vs/workbench/common/views'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { SerializedError } from 'vs/base/common/errors'; -import { IStat, FileChangeType, IWatchOptions, FileSystemProviderCapabilities, FileWriteOptions, FileType, FileOverwriteOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; -import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +import * as editorCommon from 'vs/editor/common/editorCommon'; import { ISingleEditOperation } from 'vs/editor/common/model'; -import { IPatternInfo, IRawSearchQuery, IRawFileMatch2, ISearchCompleteStats } from 'vs/platform/search/common/search'; +import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; +import * as modes from 'vs/editor/common/modes'; +import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; +import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; +import { ConfigurationTarget, IConfigurationData, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; import { LogLevel } from 'vs/platform/log/common/log'; -import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO } from 'vs/workbench/api/shared/tasks'; +import { IMarkerData } from 'vs/platform/markers/common/markers'; +import { IPickOptions, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IPatternInfo, IQueryOptions, IRawFileMatch2, IRawSearchQuery, ISearchCompleteStats } from 'vs/platform/search/common/search'; +import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; +import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; +import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { EndOfLine, IFileOperationOptions, TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; +import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; +import { TaskDTO, TaskExecutionDTO, TaskFilterDTO, TaskHandleDTO, TaskProcessEndedDTO, TaskProcessStartedDTO, TaskSystemInfoDTO } from 'vs/workbench/api/shared/tasks'; +import { ITreeItem } from 'vs/workbench/common/views'; +import { IAdapterExecutable, IConfig, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { TaskSet } from 'vs/workbench/parts/tasks/common/tasks'; import { ITerminalDimensions } from 'vs/workbench/parts/terminal/common/terminal'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol, ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier'; +import { IProgressOptions, IProgressStep } from 'vs/workbench/services/progress/common/progress'; +import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import * as vscode from 'vscode'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -471,7 +462,8 @@ export interface ExtHostUrlsShape { } export interface MainThreadWorkspaceShape extends IDisposable { - $startSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable; + $startFileSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable; + $startTextSearch(query: IPatternInfo, options: IQueryOptions, requestId: number): TPromise; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable; @@ -671,6 +663,7 @@ export interface ExtHostTreeViewsShape { export interface ExtHostWorkspaceShape { $acceptWorkspaceData(workspace: IWorkspaceData): void; + $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void; } export interface ExtHostFileSystemShape { diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 7fd58a461ac..afc18e7968a 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -4,22 +4,25 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; -import { normalize } from 'vs/base/common/paths'; +import { posix, relative } from 'path'; import { delta as arrayDelta } from 'vs/base/common/arrays'; -import { relative, posix } from 'path'; -import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext, MainThreadMessageServiceShape } from './extHost.protocol'; -import * as vscode from 'vscode'; -import { compare } from 'vs/base/common/strings'; +import { Emitter, Event } from 'vs/base/common/event'; import { TernarySearchTree } from 'vs/base/common/map'; -import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; +import { normalize } from 'vs/base/common/paths'; import { isLinux } from 'vs/base/common/platform'; -import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; +import { compare } from 'vs/base/common/strings'; +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; -import { Severity } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IQueryOptions, IRawFileMatch2 } from 'vs/platform/search/common/search'; +import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Range } from 'vs/workbench/api/node/extHostTypes'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; +import { ExtHostWorkspaceShape, IMainContext, IWorkspaceData, MainContext, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; function isFolderEqual(folderA: URI, folderB: URI): boolean { return isEqual(folderA, folderB, !isLinux); @@ -145,6 +148,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; + private readonly _activeSearchCallbacks = []; + constructor( mainContext: IMainContext, data: IWorkspaceData, @@ -358,13 +363,62 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } } - const result = this._proxy.$startSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, requestId); + const result = this._proxy.$startFileSearch(includePattern, includeFolder, excludePatternOrDisregardExcludes, maxResults, requestId); if (token) { token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId)); } return result.then(data => Array.isArray(data) ? data.map(URI.revive) : []); } + findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, callback: (result: vscode.TextSearchResult) => void, extensionId: string, token?: vscode.CancellationToken) { + this._logService.trace(`extHostWorkspace#findTextInFiles: textSearch, extension: ${extensionId}, entryPoint: findTextInFiles`); + + const requestId = ExtHostWorkspace._requestIdPool++; + + const queryOptions: IQueryOptions = { + ignoreSymlinks: typeof options.followSymlinks === 'boolean' ? !options.followSymlinks : undefined, + disregardIgnoreFiles: typeof options.useIgnoreFiles === 'boolean' ? !options.useIgnoreFiles : undefined, + fileEncoding: options.encoding, + maxResults: options.maxResults, + + // TODO + // includePattern: options.includes + // excludePattern: options.excludes + }; + + this._activeSearchCallbacks[requestId] = p => { + p.lineMatches.forEach(lineMatch => { + lineMatch.offsetAndLengths.forEach(offsetAndLength => { + const range = new Range(lineMatch.lineNumber, offsetAndLength[0], lineMatch.lineNumber, offsetAndLength[0] + offsetAndLength[1]); + callback({ + path: URI.revive(p.resource).fsPath, + preview: { text: lineMatch.preview, match: range }, + range + }); + }); + }); + }; + + if (token) { + token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId)); + } + + return this._proxy.$startTextSearch(query, queryOptions, requestId).then( + () => { + delete this._activeSearchCallbacks[requestId]; + }, + err => { + delete this._activeSearchCallbacks[requestId]; + return TPromise.wrapError(err); + }); + } + + $handleTextSearchResult(result: IRawFileMatch2, requestId: number): void { + if (this._activeSearchCallbacks[requestId]) { + this._activeSearchCallbacks[requestId](result); + } + } + saveAll(includeUntitled?: boolean): Thenable { return this._proxy.$saveAll(includeUntitled); }