Merge branch 'master' into pr/mechatroner/55107-1

This commit is contained in:
Johannes Rieken
2018-08-27 14:15:53 +02:00
997 changed files with 44182 additions and 27830 deletions

View File

@@ -5,6 +5,7 @@
'use strict';
import URI from 'vs/base/common/uri';
import { isMalformedFileUri } from 'vs/base/common/resources';
import * as vscode from 'vscode';
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands';
@@ -48,6 +49,12 @@ export class OpenFolderAPICommand {
if (!uri) {
return executor.executeCommand('_files.pickFolderAndOpen', forceNewWindow);
}
let correctedUri = isMalformedFileUri(uri);
if (correctedUri) {
// workaround for #55916 and #55891, will be removed in 1.28
console.warn(`'vscode.openFolder' command invoked with an invalid URI (file:// scheme missing): '${uri}'. Converted to a 'file://' URI: ${correctedUri}`);
uri = correctedUri;
}
return executor.executeCommand('_files.windowOpen', [uri], forceNewWindow);
}

View File

@@ -114,7 +114,7 @@ export function createApiFactory(
rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace);
rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration);
const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol));
const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, schemeTransformer, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics));
const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, schemeTransformer, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics, extHostLogService));
const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures));
const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostDocumentsAndEditors));
const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands));
@@ -227,7 +227,14 @@ export function createApiFactory(
get language() { return platform.language; },
get appName() { return product.nameLong; },
get appRoot() { return initData.environment.appRoot; },
get logLevel() { return extHostLogService.getLevel(); }
get logLevel() {
checkProposedApiEnabled(extension);
return extHostLogService.getLevel();
},
get onDidChangeLogLevel() {
checkProposedApiEnabled(extension);
return extHostLogService.onDidChangeLogLevel;
}
});
// namespace: extensions
@@ -266,7 +273,7 @@ export function createApiFactory(
return score(typeConverters.LanguageSelector.from(selector), document.uri, document.languageId, true);
},
registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable {
return extHostLanguageFeatures.registerCodeActionProvider(checkSelector(selector), provider, metadata);
return extHostLanguageFeatures.registerCodeActionProvider(checkSelector(selector), provider, extension, metadata);
},
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
return extHostLanguageFeatures.registerCodeLensProvider(checkSelector(selector), provider);
@@ -429,7 +436,7 @@ export function createApiFactory(
return extHostOutputService.createOutputChannel(name);
},
createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel {
return extHostWebviews.createWebview(viewType, title, showOptions, options, extension.extensionLocation);
return extHostWebviews.createWebview(extension.extensionLocation, viewType, title, showOptions, options);
},
createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal {
if (typeof nameOrOptions === 'object') {
@@ -437,9 +444,9 @@ export function createApiFactory(
}
return extHostTerminalService.createTerminal(<string>nameOrOptions, shellPath, shellArgs);
},
createTerminalRenderer(name: string): vscode.TerminalRenderer {
createTerminalRenderer: proposedApiFunction(extension, (name: string) => {
return extHostTerminalService.createTerminalRenderer(name);
},
}),
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.Disposable {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider);
},
@@ -467,11 +474,6 @@ export function createApiFactory(
},
};
// namespace: QuickInputButtons
const QuickInputButtons: typeof vscode.QuickInputButtons = {
Back: extHostQuickOpen.backButton,
};
// namespace: workspace
const workspace: typeof vscode.workspace = {
get rootPath() {
@@ -487,7 +489,7 @@ export function createApiFactory(
return extHostWorkspace.getWorkspaceFolders();
},
get name() {
return extHostWorkspace.workspace ? extHostWorkspace.workspace.name : undefined;
return extHostWorkspace.name;
},
set name(value) {
throw errors.readonly();
@@ -586,8 +588,18 @@ export function createApiFactory(
registerFileSystemProvider(scheme, provider, options) {
return extHostFileSystem.registerFileSystemProvider(scheme, provider, options);
},
registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => {
return extHostSearch.registerSearchProvider(scheme, provider);
registerFileSearchProvider: proposedApiFunction(extension, (scheme, provider) => {
return extHostSearch.registerFileSearchProvider(scheme, provider);
}),
registerSearchProvider: proposedApiFunction(extension, () => {
// Temp for live share in Insiders
return { dispose: () => { } };
}),
registerTextSearchProvider: proposedApiFunction(extension, (scheme, provider) => {
return extHostSearch.registerTextSearchProvider(scheme, provider);
}),
registerFileIndexProvider: proposedApiFunction(extension, (scheme, provider) => {
return extHostSearch.registerFileIndexProvider(scheme, provider);
}),
registerDocumentCommentProvider: proposedApiFunction(extension, (provider: vscode.DocumentCommentProvider) => {
return exthostCommentProviders.registerDocumentCommentProvider(provider);
@@ -684,39 +696,48 @@ export function createApiFactory(
version: pkg.version,
// namespaces
commands,
debug,
env,
extensions,
languages,
scm,
tasks,
window,
workspace,
scm,
debug,
tasks,
// types
Breakpoint: extHostTypes.Breakpoint,
CancellationTokenSource: CancellationTokenSource,
CodeAction: extHostTypes.CodeAction,
CodeActionKind: extHostTypes.CodeActionKind,
CodeActionTrigger: extHostTypes.CodeActionTrigger,
CodeLens: extHostTypes.CodeLens,
Color: extHostTypes.Color,
ColorPresentation: extHostTypes.ColorPresentation,
ColorInformation: extHostTypes.ColorInformation,
CodeActionTrigger: extHostTypes.CodeActionTrigger,
EndOfLine: extHostTypes.EndOfLine,
ColorPresentation: extHostTypes.ColorPresentation,
CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState,
CompletionItem: extHostTypes.CompletionItem,
CompletionItemKind: extHostTypes.CompletionItemKind,
CompletionList: extHostTypes.CompletionList,
CompletionTriggerKind: extHostTypes.CompletionTriggerKind,
ConfigurationTarget: extHostTypes.ConfigurationTarget,
DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable,
DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior,
Diagnostic: extHostTypes.Diagnostic,
DiagnosticRelatedInformation: extHostTypes.DiagnosticRelatedInformation,
DiagnosticTag: extHostTypes.DiagnosticTag,
DiagnosticSeverity: extHostTypes.DiagnosticSeverity,
DiagnosticTag: extHostTypes.DiagnosticTag,
Disposable: extHostTypes.Disposable,
DocumentHighlight: extHostTypes.DocumentHighlight,
DocumentHighlightKind: extHostTypes.DocumentHighlightKind,
DocumentLink: extHostTypes.DocumentLink,
DocumentSymbol: extHostTypes.DocumentSymbol,
EndOfLine: extHostTypes.EndOfLine,
EventEmitter: Emitter,
FileChangeType: extHostTypes.FileChangeType,
FileSystemError: extHostTypes.FileSystemError,
FileType: files.FileType,
FoldingRange: extHostTypes.FoldingRange,
FoldingRangeKind: extHostTypes.FoldingRangeKind,
FunctionBreakpoint: extHostTypes.FunctionBreakpoint,
Hover: extHostTypes.Hover,
IndentAction: languageConfiguration.IndentAction,
@@ -726,52 +747,41 @@ export function createApiFactory(
OverviewRulerLane: OverviewRulerLane,
ParameterInformation: extHostTypes.ParameterInformation,
Position: extHostTypes.Position,
QuickInputButtons,
ProcessExecution: extHostTypes.ProcessExecution,
ProgressLocation: extHostTypes.ProgressLocation,
QuickInputButtons: extHostTypes.QuickInputButtons,
Range: extHostTypes.Range,
RelativePattern: extHostTypes.RelativePattern,
Selection: extHostTypes.Selection,
ShellExecution: extHostTypes.ShellExecution,
ShellQuoting: extHostTypes.ShellQuoting,
SignatureHelp: extHostTypes.SignatureHelp,
SignatureInformation: extHostTypes.SignatureInformation,
SnippetString: extHostTypes.SnippetString,
SourceBreakpoint: extHostTypes.SourceBreakpoint,
SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType,
StatusBarAlignment: extHostTypes.StatusBarAlignment,
SymbolInformation: extHostTypes.SymbolInformation,
DocumentSymbol: extHostTypes.DocumentSymbol,
SymbolKind: extHostTypes.SymbolKind,
SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType,
Task: extHostTypes.Task,
TaskGroup: extHostTypes.TaskGroup,
TaskPanelKind: extHostTypes.TaskPanelKind,
TaskRevealKind: extHostTypes.TaskRevealKind,
TaskScope: extHostTypes.TaskScope,
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
TextEdit: extHostTypes.TextEdit,
TextEditorCursorStyle: TextEditorCursorStyle,
TextEditorLineNumbersStyle: extHostTypes.TextEditorLineNumbersStyle,
TextEditorRevealType: extHostTypes.TextEditorRevealType,
TextEditorSelectionChangeKind: extHostTypes.TextEditorSelectionChangeKind,
DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior,
ThemeColor: extHostTypes.ThemeColor,
ThemeIcon: extHostTypes.ThemeIcon,
TreeItem: extHostTypes.TreeItem,
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
Uri: URI,
ViewColumn: extHostTypes.ViewColumn,
WorkspaceEdit: extHostTypes.WorkspaceEdit,
ProgressLocation: extHostTypes.ProgressLocation,
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
ThemeIcon: extHostTypes.ThemeIcon,
TreeItem: extHostTypes.TreeItem,
ThemeColor: extHostTypes.ThemeColor,
// functions
TaskRevealKind: extHostTypes.TaskRevealKind,
TaskPanelKind: extHostTypes.TaskPanelKind,
TaskGroup: extHostTypes.TaskGroup,
ProcessExecution: extHostTypes.ProcessExecution,
ShellExecution: extHostTypes.ShellExecution,
ShellQuoting: extHostTypes.ShellQuoting,
TaskScope: extHostTypes.TaskScope,
Task: extHostTypes.Task,
ConfigurationTarget: extHostTypes.ConfigurationTarget,
RelativePattern: extHostTypes.RelativePattern,
FileChangeType: extHostTypes.FileChangeType,
FileType: files.FileType,
FileSystemError: extHostTypes.FileSystemError,
FoldingRange: extHostTypes.FoldingRange,
FoldingRangeKind: extHostTypes.FoldingRangeKind,
CommentThreadCollapsibleState: extHostTypes.CommentThreadCollapsibleState
};
};
}

View File

@@ -22,10 +22,11 @@ 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 { LabelRules } from 'vs/platform/label/common/label';
import { LogLevel } from 'vs/platform/log/common/log';
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 { IPatternInfo, IQueryOptions, IRawFileMatch2, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } 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';
@@ -46,7 +47,7 @@ export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
appRoot: string;
appSettingsHome: string;
extensionDevelopmentPath: string;
extensionDevelopmentLocationURI: URI;
extensionTestsPath: string;
}
@@ -426,11 +427,17 @@ export interface MainThreadTelemetryShape extends IDisposable {
export type WebviewPanelHandle = string;
export interface WebviewPanelShowOptions {
readonly viewColumn?: EditorViewColumn;
readonly preserveFocus?: boolean;
}
export interface MainThreadWebviewsShape extends IDisposable {
$createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, viewOptions: { viewColumn: EditorViewColumn, preserveFocus: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionLocation: UriComponents): void;
$createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionLocation: UriComponents): void;
$disposeWebview(handle: WebviewPanelHandle): void;
$reveal(handle: WebviewPanelHandle, viewColumn: EditorViewColumn | null, preserveFocus: boolean): void;
$reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void;
$setTitle(handle: WebviewPanelHandle, value: string): void;
$setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void;
$setHtml(handle: WebviewPanelHandle, value: string): void;
$setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void;
$postMessage(handle: WebviewPanelHandle, value: any): Thenable<boolean>;
@@ -464,6 +471,7 @@ export interface ExtHostUrlsShape {
export interface MainThreadWorkspaceShape extends IDisposable {
$startFileSearch(includePattern: string, includeFolder: string, excludePatternOrDisregardExcludes: string | false, maxResults: number, requestId: number): Thenable<UriComponents[]>;
$startTextSearch(query: IPatternInfo, options: IQueryOptions, requestId: number): TPromise<void>;
$checkExists(query: ISearchQuery, requestId: number): TPromise<boolean>;
$cancelSearch(requestId: number): Thenable<boolean>;
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable<void>;
@@ -477,11 +485,14 @@ export interface IFileChangeDto {
export interface MainThreadFileSystemShape extends IDisposable {
$registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void;
$unregisterProvider(handle: number): void;
$setUriFormatter(scheme: string, formatter: LabelRules): void;
$onFileSystemChange(handle: number, resource: IFileChangeDto[]): void;
}
export interface MainThreadSearchShape extends IDisposable {
$registerSearchProvider(handle: number, scheme: string): void;
$registerFileSearchProvider(handle: number, scheme: string): void;
$registerTextSearchProvider(handle: number, scheme: string): void;
$registerFileIndexProvider(handle: number, scheme: string): void;
$unregisterProvider(handle: number): void;
$handleFileMatch(handle: number, session: number, data: UriComponents[]): void;
$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void;
@@ -670,8 +681,8 @@ export interface ExtHostWorkspaceShape {
export interface ExtHostFileSystemShape {
$stat(handle: number, resource: UriComponents): TPromise<IStat>;
$readdir(handle: number, resource: UriComponents): TPromise<[string, FileType][]>;
$readFile(handle: number, resource: UriComponents): TPromise<string>;
$writeFile(handle: number, resource: UriComponents, base64Encoded: string, opts: FileWriteOptions): TPromise<void>;
$readFile(handle: number, resource: UriComponents): TPromise<Buffer>;
$writeFile(handle: number, resource: UriComponents, content: Buffer, opts: FileWriteOptions): TPromise<void>;
$rename(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): TPromise<void>;
$copy(handle: number, resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): TPromise<void>;
$mkdir(handle: number, resource: UriComponents): TPromise<void>;
@@ -682,7 +693,7 @@ export interface ExtHostFileSystemShape {
export interface ExtHostSearchShape {
$provideFileSearchResults(handle: number, session: number, query: IRawSearchQuery): TPromise<ISearchCompleteStats>;
$clearCache(handle: number, cacheKey: string): TPromise<void>;
$clearCache(cacheKey: string): TPromise<void>;
$provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, query: IRawSearchQuery): TPromise<ISearchCompleteStats>;
}
@@ -875,6 +886,7 @@ export interface ExtHostSCMShape {
$onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise<void>;
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise<void>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): TPromise<[string, number] | undefined>;
$setSelectedSourceControls(selectedSourceControlHandles: number[]): TPromise<void>;
}
export interface ExtHostTaskShape {

View File

@@ -11,21 +11,26 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
import { asWinJsPromise } from 'vs/base/common/async';
interface ProviderData {
provider: vscode.DecorationProvider;
extensionId: string;
}
export class ExtHostDecorations implements ExtHostDecorationsShape {
private static _handlePool = 0;
private readonly _provider = new Map<number, vscode.DecorationProvider>();
private readonly _provider = new Map<number, ProviderData>();
private readonly _proxy: MainThreadDecorationsShape;
constructor(mainContext: IMainContext) {
this._proxy = mainContext.getProxy(MainContext.MainThreadDecorations);
}
registerDecorationProvider(provider: vscode.DecorationProvider, label: string): vscode.Disposable {
registerDecorationProvider(provider: vscode.DecorationProvider, extensionId: string): vscode.Disposable {
const handle = ExtHostDecorations._handlePool++;
this._provider.set(handle, provider);
this._proxy.$registerDecorationProvider(handle, label);
this._provider.set(handle, { provider, extensionId });
this._proxy.$registerDecorationProvider(handle, extensionId);
const listener = provider.onDidChangeDecorations(e => {
this._proxy.$onDidChange(handle, !e ? null : Array.isArray(e) ? e : [e]);
@@ -42,13 +47,16 @@ export class ExtHostDecorations implements ExtHostDecorationsShape {
const result: DecorationReply = Object.create(null);
return TPromise.join(requests.map(request => {
const { handle, uri, id } = request;
const provider = this._provider.get(handle);
if (!provider) {
if (!this._provider.has(handle)) {
// might have been unregistered in the meantime
return void 0;
}
const { provider, extensionId } = this._provider.get(handle);
return asWinJsPromise(token => provider.provideDecoration(URI.revive(uri), token)).then(data => {
result[id] = data && <DecorationData>[data.priority, data.bubble, data.title, data.abbreviation, data.color, data.source];
if (data && data.letter && data.letter.length !== 1) {
console.warn(`INVALID decoration from extension '${extensionId}'. The 'letter' must be set and be one character, not '${data.letter}'.`);
}
result[id] = data && <DecorationData>[data.priority, data.bubble, data.title, data.letter, data.color, data.source];
}, err => {
console.error(err);
});

View File

@@ -10,7 +10,6 @@ import Severity from 'vs/base/common/severity';
import { TPromise } from 'vs/base/common/winjs.base';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ExtHostLogger } from 'vs/workbench/api/node/extHostLogService';
const hasOwnProperty = Object.hasOwnProperty;
const NO_OP_VOID_PROMISE = TPromise.wrap<void>(void 0);
@@ -27,8 +26,7 @@ export interface IExtensionContext {
extensionPath: string;
storagePath: string;
asAbsolutePath(relativePath: string): string;
logger: ExtHostLogger;
readonly logDirectory: string;
readonly logPath: string;
}
/**

View File

@@ -12,7 +12,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ExtHostStorage } from 'vs/workbench/api/node/extHostStorage';
import { createApiFactory, initializeExtensionApi, checkProposedApiEnabled } from 'vs/workbench/api/node/extHost.api.impl';
import { createApiFactory, initializeExtensionApi } from 'vs/workbench/api/node/extHost.api.impl';
import { MainContext, MainThreadExtensionServiceShape, IWorkspaceData, IEnvironment, IInitData, ExtHostExtensionServiceShape, MainThreadTelemetryShape, IMainContext } from './extHost.protocol';
import { IExtensionMemento, ExtensionsActivator, ActivatedExtension, IExtensionAPI, IExtensionContext, EmptyExtension, IExtensionModule, ExtensionActivationTimesBuilder, ExtensionActivationTimes, ExtensionActivationReason, ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator';
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
@@ -361,14 +361,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape {
get extensionPath() { return extensionDescription.extensionLocation.fsPath; },
storagePath: this._storagePath.value(extensionDescription),
asAbsolutePath: (relativePath: string) => { return join(extensionDescription.extensionLocation.fsPath, relativePath); },
get logger() {
checkProposedApiEnabled(extensionDescription);
return that._extHostLogService.getExtLogger(extensionDescription.id);
},
get logDirectory() {
checkProposedApiEnabled(extensionDescription);
return that._extHostLogService.getLogDirectory(extensionDescription.id);
}
logPath: that._extHostLogService.getLogDirectory(extensionDescription.id)
});
});
}

View File

@@ -15,6 +15,7 @@ import { values } from 'vs/base/common/map';
import { Range, FileChangeType } from 'vs/workbench/api/node/extHostTypes';
import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures';
import { Schemas } from 'vs/base/common/network';
import { LabelRules } from 'vs/platform/label/common/label';
class FsLinkProvider implements vscode.DocumentLinkProvider {
@@ -141,44 +142,48 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
});
}
setUriFormatter(scheme: string, formatter: LabelRules): void {
this._proxy.$setUriFormatter(scheme, formatter);
}
private static _asIStat(stat: vscode.FileStat): files.IStat {
const { type, ctime, mtime, size } = stat;
return { type, ctime, mtime, size };
}
$stat(handle: number, resource: UriComponents): TPromise<files.IStat, any> {
$stat(handle: number, resource: UriComponents): TPromise<files.IStat> {
return asWinJsPromise(() => this._fsProvider.get(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat);
}
$readdir(handle: number, resource: UriComponents): TPromise<[string, files.FileType][], any> {
$readdir(handle: number, resource: UriComponents): TPromise<[string, files.FileType][]> {
return asWinJsPromise(() => this._fsProvider.get(handle).readDirectory(URI.revive(resource)));
}
$readFile(handle: number, resource: UriComponents): TPromise<string> {
$readFile(handle: number, resource: UriComponents): TPromise<Buffer> {
return asWinJsPromise(() => {
return this._fsProvider.get(handle).readFile(URI.revive(resource));
}).then(data => {
return Buffer.isBuffer(data) ? data.toString('base64') : Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('base64');
return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
});
}
$writeFile(handle: number, resource: UriComponents, base64Content: string, opts: files.FileWriteOptions): TPromise<void, any> {
return asWinJsPromise(() => this._fsProvider.get(handle).writeFile(URI.revive(resource), Buffer.from(base64Content, 'base64'), opts));
$writeFile(handle: number, resource: UriComponents, content: Buffer, opts: files.FileWriteOptions): TPromise<void> {
return asWinJsPromise(() => this._fsProvider.get(handle).writeFile(URI.revive(resource), content, opts));
}
$delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): TPromise<void, any> {
$delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): TPromise<void> {
return asWinJsPromise(() => this._fsProvider.get(handle).delete(URI.revive(resource), opts));
}
$rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise<void, any> {
$rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise<void> {
return asWinJsPromise(() => this._fsProvider.get(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts));
}
$copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise<void, any> {
$copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): TPromise<void> {
return asWinJsPromise(() => this._fsProvider.get(handle).copy(URI.revive(oldUri), URI.revive(newUri), opts));
}
$mkdir(handle: number, resource: UriComponents): TPromise<void, any> {
$mkdir(handle: number, resource: UriComponents): TPromise<void> {
return asWinJsPromise(() => this._fsProvider.get(handle).createDirectory(URI.revive(resource)));
}

View File

@@ -25,6 +25,7 @@ import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { isObject } from 'vs/base/common/types';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
// --- adapter
@@ -273,7 +274,9 @@ class CodeActionAdapter {
private readonly _documents: ExtHostDocuments,
private readonly _commands: CommandsConverter,
private readonly _diagnostics: ExtHostDiagnostics,
private readonly _provider: vscode.CodeActionProvider
private readonly _provider: vscode.CodeActionProvider,
private readonly _logService: ILogService,
private readonly _extensionId: string
) { }
provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext): TPromise<CodeActionDto[]> {
@@ -314,6 +317,14 @@ class CodeActionAdapter {
command: this._commands.toInternal(candidate),
});
} else {
if (codeActionContext.only) {
if (!candidate.kind) {
this._logService.warn(`${this._extensionId} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`);
} else if (!codeActionContext.only.contains(candidate.kind)) {
this._logService.warn(`${this._extensionId} -Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`);
}
}
// new school: convert code action
result.push({
title: candidate.title,
@@ -838,6 +849,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
private _heapService: ExtHostHeapService;
private _diagnostics: ExtHostDiagnostics;
private _adapter = new Map<number, Adapter>();
private readonly _logService: ILogService;
constructor(
mainContext: IMainContext,
@@ -845,7 +857,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
documents: ExtHostDocuments,
commands: ExtHostCommands,
heapMonitor: ExtHostHeapService,
diagnostics: ExtHostDiagnostics
diagnostics: ExtHostDiagnostics,
logService: ILogService
) {
this._schemeTransformer = schemeTransformer;
this._proxy = mainContext.getProxy(MainContext.MainThreadLanguageFeatures);
@@ -853,6 +866,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
this._commands = commands;
this._heapService = heapMonitor;
this._diagnostics = diagnostics;
this._logService = logService;
}
private _transformDocumentSelector(selector: vscode.DocumentSelector): ISerializedDocumentFilter[] {
@@ -1024,8 +1038,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
// --- quick fix
registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable {
const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider));
registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, extension?: IExtensionDescription, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable {
const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension ? extension.id : ''));
this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), metadata && metadata.providedCodeActionKinds ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined);
return this._createDisposable(handle);
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import { join } from 'vs/base/common/paths';
import { LogLevel } from 'vs/workbench/api/node/extHostTypes';
import { ILogService, DelegatedLogService } from 'vs/platform/log/common/log';
@@ -14,8 +13,6 @@ import { ExtHostLogServiceShape } from 'vs/workbench/api/node/extHost.protocol';
export class ExtHostLogService extends DelegatedLogService implements ILogService, ExtHostLogServiceShape {
private _loggers: Map<string, ExtHostLogger> = new Map();
constructor(
private _windowId: number,
logLevel: LogLevel,
@@ -28,54 +25,7 @@ export class ExtHostLogService extends DelegatedLogService implements ILogServic
this.setLevel(level);
}
getExtLogger(extensionID: string): ExtHostLogger {
let logger = this._loggers.get(extensionID);
if (!logger) {
logger = this.createLogger(extensionID);
this._loggers.set(extensionID, logger);
}
return logger;
}
getLogDirectory(extensionID: string): string {
return join(this._logsPath, `${extensionID}_${this._windowId}`);
}
private createLogger(extensionID: string): ExtHostLogger {
const logsDirPath = this.getLogDirectory(extensionID);
const logService = createSpdLogService(extensionID, this.getLevel(), logsDirPath);
this._register(this.onDidChangeLogLevel(level => logService.setLevel(level)));
return new ExtHostLogger(logService);
}
}
export class ExtHostLogger implements vscode.Logger {
constructor(
private readonly _logService: ILogService
) { }
trace(message: string, ...args: any[]): void {
return this._logService.trace(message, ...args);
}
debug(message: string, ...args: any[]): void {
return this._logService.debug(message, ...args);
}
info(message: string, ...args: any[]): void {
return this._logService.info(message, ...args);
}
warn(message: string, ...args: any[]): void {
return this._logService.warn(message, ...args);
}
error(message: string | Error, ...args: any[]): void {
return this._logService.error(message, ...args);
}
critical(message: string | Error, ...args: any[]): void {
return this._logService.critical(message, ...args);
}
}

View File

@@ -14,9 +14,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
import { InputBox, InputBoxOptions, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode';
import { ExtHostQuickOpenShape, IMainContext, MainContext, MainThreadQuickOpenShape, TransferQuickPickItems, TransferQuickInput, TransferQuickInputButton } from './extHost.protocol';
import URI from 'vs/base/common/uri';
import { ThemeIcon } from 'vs/workbench/api/node/extHostTypes';
const backButton: QuickInputButton = { iconPath: 'back.svg' };
import { ThemeIcon, QuickInputButtons } from 'vs/workbench/api/node/extHostTypes';
export type Item = string | QuickPickItem;
@@ -153,8 +151,6 @@ export class ExtHostQuickOpen implements ExtHostQuickOpenShape {
// ---- QuickInput
backButton = backButton;
createQuickPick<T extends QuickPickItem>(extensionId: string): QuickPick<T> {
const session = new ExtHostQuickPick(this._proxy, extensionId, () => this._sessions.delete(session._id));
this._sessions.set(session._id, session);
@@ -328,14 +324,14 @@ class ExtHostQuickInput implements QuickInput {
this._buttons = buttons.slice();
this._handlesToButtons.clear();
buttons.forEach((button, i) => {
const handle = button === backButton ? -1 : i;
const handle = button === QuickInputButtons.Back ? -1 : i;
this._handlesToButtons.set(handle, button);
});
this.update({
buttons: buttons.map<TransferQuickInputButton>((button, i) => ({
iconPath: getIconUris(button.iconPath),
tooltip: button.tooltip,
handle: button === backButton ? -1 : i,
handle: button === QuickInputButtons.Back ? -1 : i,
}))
});
}

View File

@@ -395,6 +395,15 @@ class ExtHostSourceControl implements vscode.SourceControl {
this._proxy.$updateSourceControl(this.handle, { statusBarCommands: internal });
}
private _selected: boolean = false;
get selected(): boolean {
return this._selected;
}
private _onDidChangeSelection = new Emitter<boolean>();
readonly onDidChangeSelection = this._onDidChangeSelection.event;
private handle: number = ExtHostSourceControl._handlePool++;
constructor(
@@ -454,6 +463,11 @@ class ExtHostSourceControl implements vscode.SourceControl {
return this._groups.get(handle);
}
setSelectionState(selected: boolean): void {
this._selected = selected;
this._onDidChangeSelection.fire(selected);
}
dispose(): void {
this._groups.forEach(group => group.dispose());
this._proxy.$unregisterSourceControl(this.handle);
@@ -471,6 +485,8 @@ export class ExtHostSCM implements ExtHostSCMShape {
private _onDidChangeActiveProvider = new Emitter<vscode.SourceControl>();
get onDidChangeActiveProvider(): Event<vscode.SourceControl> { return this._onDidChangeActiveProvider.event; }
private _selectedSourceControlHandles = new Set<number>();
constructor(
mainContext: IMainContext,
private _commands: ExtHostCommands,
@@ -607,4 +623,41 @@ export class ExtHostSCM implements ExtHostSCMShape {
return TPromise.as<[string, number]>([result.message, result.type]);
});
}
$setSelectedSourceControls(selectedSourceControlHandles: number[]): TPromise<void> {
this.logService.trace('ExtHostSCM#$setSelectedSourceControls', selectedSourceControlHandles);
const set = new Set<number>();
for (const handle of selectedSourceControlHandles) {
set.add(handle);
}
set.forEach(handle => {
if (!this._selectedSourceControlHandles.has(handle)) {
const sourceControl = this._sourceControls.get(handle);
if (!sourceControl) {
return;
}
sourceControl.setSelectionState(true);
}
});
this._selectedSourceControlHandles.forEach(handle => {
if (!set.has(handle)) {
const sourceControl = this._sourceControls.get(handle);
if (!sourceControl) {
return;
}
sourceControl.setSelectionState(false);
}
});
this._selectedSourceControlHandles = set;
return TPromise.as(null);
}
}

View File

@@ -0,0 +1,730 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import * as arrays from 'vs/base/common/arrays';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as glob from 'vs/base/common/glob';
import * as resources from 'vs/base/common/resources';
import { StopWatch } from 'vs/base/common/stopwatch';
import * as strings from 'vs/base/common/strings';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { ICachedSearchStats, IFileMatch, IFolderQuery, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, IFileSearchStats, IFileIndexProviderStats } from 'vs/platform/search/common/search';
import * as vscode from 'vscode';
import { canceled } from 'vs/base/common/errors';
export interface IInternalFileMatch {
base: URI;
original?: URI;
relativePath?: string; // Not present for extraFiles or absolute path matches
basename: string;
size?: number;
}
/**
* Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
*/
export function resolvePatternsForProvider(globalPattern: glob.IExpression, folderPattern: glob.IExpression): string[] {
const merged = {
...(globalPattern || {}),
...(folderPattern || {})
};
return Object.keys(merged)
.filter(key => {
const value = merged[key];
return typeof value === 'boolean' && value;
});
}
export class QueryGlobTester {
private _excludeExpression: glob.IExpression;
private _parsedExcludeExpression: glob.ParsedExpression;
private _parsedIncludeExpression: glob.ParsedExpression;
constructor(config: ISearchQuery, folderQuery: IFolderQuery) {
this._excludeExpression = {
...(config.excludePattern || {}),
...(folderQuery.excludePattern || {})
};
this._parsedExcludeExpression = glob.parse(this._excludeExpression);
// Empty includeExpression means include nothing, so no {} shortcuts
let includeExpression: glob.IExpression = config.includePattern;
if (folderQuery.includePattern) {
if (includeExpression) {
includeExpression = {
...includeExpression,
...folderQuery.includePattern
};
} else {
includeExpression = folderQuery.includePattern;
}
}
if (includeExpression) {
this._parsedIncludeExpression = glob.parse(includeExpression);
}
}
/**
* Guaranteed sync - siblingsFn should not return a promise.
*/
public includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean {
if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) {
return false;
}
if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) {
return false;
}
return true;
}
/**
* Guaranteed async.
*/
public includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise<boolean>): TPromise<boolean> {
const excludeP = this._parsedExcludeExpression ?
TPromise.as(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
TPromise.wrap(false);
return excludeP.then(excluded => {
if (excluded) {
return false;
}
return this._parsedIncludeExpression ?
TPromise.as(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
TPromise.wrap(true);
}).then(included => {
return included;
});
}
public hasSiblingExcludeClauses(): boolean {
return hasSiblingClauses(this._excludeExpression);
}
}
function hasSiblingClauses(pattern: glob.IExpression): boolean {
for (let key in pattern) {
if (typeof pattern[key] !== 'boolean') {
return true;
}
}
return false;
}
export interface IDirectoryEntry {
base: URI;
relativePath: string;
basename: string;
}
export interface IDirectoryTree {
rootEntries: IDirectoryEntry[];
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}
interface IInternalSearchComplete<T = IFileSearchStats> {
limitHit: boolean;
results: IInternalFileMatch[];
stats: T;
}
export class FileIndexSearchEngine {
private filePattern: string;
private normalizedFilePatternLowercase: string;
private includePattern: glob.ParsedExpression;
private maxResults: number;
private exists: boolean;
private isLimitHit: boolean;
private resultCount: number;
private isCanceled: boolean;
private filesWalked = 0;
private dirsWalked = 0;
private activeCancellationTokens: Set<CancellationTokenSource>;
private globalExcludePattern: glob.ParsedExpression;
constructor(private config: ISearchQuery, private provider: vscode.FileIndexProvider) {
this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || null;
this.exists = config.exists;
this.resultCount = 0;
this.isLimitHit = false;
this.activeCancellationTokens = new Set<CancellationTokenSource>();
if (this.filePattern) {
this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
}
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
}
public cancel(): void {
this.isCanceled = true;
this.activeCancellationTokens.forEach(t => t.cancel());
this.activeCancellationTokens = new Set();
}
public search(_onResult: (match: IInternalFileMatch) => void): TPromise<{ isLimitHit: boolean, stats: IFileIndexProviderStats }> {
if (this.config.folderQueries.length !== 1) {
throw new Error('Searches just one folder');
}
// Searches a single folder
const folderQuery = this.config.folderQueries[0];
return new TPromise<{ isLimitHit: boolean, stats: IFileIndexProviderStats }>((resolve, reject) => {
const onResult = (match: IInternalFileMatch) => {
this.resultCount++;
_onResult(match);
};
if (this.isCanceled) {
throw canceled();
}
// For each extra file
if (this.config.extraFileResources) {
this.config.extraFileResources
.forEach(extraFile => {
const extraFileStr = extraFile.toString(); // ?
const basename = path.basename(extraFileStr);
if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) {
return; // excluded
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { base: extraFile, basename });
});
}
return this.searchInFolder(folderQuery, _onResult)
.then(stats => {
resolve({
isLimitHit: this.isLimitHit,
stats
});
}, (errs: Error[]) => {
const errMsg = errs
.map(err => toErrorMessage(err))
.filter(msg => !!msg)[0];
reject(new Error(errMsg));
});
});
}
private searchInFolder(fq: IFolderQuery<URI>, onResult: (match: IInternalFileMatch) => void): TPromise<IFileIndexProviderStats> {
let cancellation = new CancellationTokenSource();
return new TPromise((resolve, reject) => {
const options = this.getSearchOptionsForFolder(fq);
const tree = this.initDirectoryTree();
const queryTester = new QueryGlobTester(this.config, fq);
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
const onProviderResult = (uri: URI) => {
if (this.isCanceled) {
return;
}
// TODO@rob - ???
const relativePath = path.relative(fq.folder.path, uri.path);
if (noSiblingsClauses) {
const basename = path.basename(uri.path);
this.matchFile(onResult, { base: fq.folder, relativePath, basename, original: uri });
return;
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(tree, fq.folder, relativePath, onResult);
};
let providerSW: StopWatch;
let providerTime: number;
let fileWalkTime: number;
new TPromise(resolve => process.nextTick(resolve))
.then(() => {
this.activeCancellationTokens.add(cancellation);
providerSW = StopWatch.create();
return this.provider.provideFileIndex(options, cancellation.token);
})
.then(results => {
providerTime = providerSW.elapsed();
const postProcessSW = StopWatch.create();
this.activeCancellationTokens.delete(cancellation);
if (this.isCanceled) {
return null;
}
results.forEach(onProviderResult);
this.matchDirectoryTree(tree, queryTester, onResult);
fileWalkTime = postProcessSW.elapsed();
return null;
}).then(
() => {
cancellation.dispose();
resolve(<IFileIndexProviderStats>{
providerTime,
fileWalkTime,
directoriesWalked: this.dirsWalked,
filesWalked: this.filesWalked
});
},
err => {
cancellation.dispose();
reject(err);
});
});
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.FileIndexOptions {
const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);
return {
folder: fq.folder,
excludes,
includes,
useIgnoreFiles: !this.config.disregardIgnoreFiles,
followSymlinks: !this.config.ignoreSymlinks
};
}
private initDirectoryTree(): IDirectoryTree {
const tree: IDirectoryTree = {
rootEntries: [],
pathToEntries: Object.create(null)
};
tree.pathToEntries['.'] = tree.rootEntries;
return tree;
}
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) {
// Support relative paths to files from a root resource (ignores excludes)
if (relativeFile === this.filePattern) {
const basename = path.basename(this.filePattern);
this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
}
function add(relativePath: string) {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
let entries = pathToEntries[dirname];
if (!entries) {
entries = pathToEntries[dirname] = [];
add(dirname);
}
entries.push({
base,
relativePath,
basename
});
}
add(relativeFile);
}
private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) {
const self = this;
const filePattern = this.filePattern;
function matchDirectory(entries: IDirectoryEntry[]) {
self.dirsWalked++;
for (let i = 0, n = entries.length; i < n; i++) {
const entry = entries[i];
const { relativePath, basename } = entry;
// Check exclude pattern
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename));
if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) {
continue;
}
const sub = pathToEntries[relativePath];
if (sub) {
matchDirectory(sub);
} else {
self.filesWalked++;
if (relativePath === filePattern) {
continue; // ignore file if its path matches with the file pattern because that is already matched above
}
self.matchFile(onResult, entry);
}
if (self.isLimitHit) {
break;
}
}
}
matchDirectory(rootEntries);
}
private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void {
if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) {
this.isLimitHit = true;
this.cancel();
}
if (!this.isLimitHit) {
onResult(candidate);
}
}
}
private isFilePatternMatch(path: string): boolean {
// Check for search pattern
if (this.filePattern) {
if (this.filePattern === '*') {
return true; // support the all-matching wildcard
}
return strings.fuzzyContains(path, this.normalizedFilePatternLowercase);
}
// No patterns means we match all
return true;
}
}
export class FileIndexSearchManager {
private static readonly BATCH_SIZE = 512;
private caches: { [cacheKey: string]: Cache; } = Object.create(null);
private readonly folderCacheKeys = new Map<string, Set<string>>();
public fileSearch(config: ISearchQuery, provider: vscode.FileIndexProvider, onBatch: (matches: IFileMatch[]) => void): TPromise<ISearchCompleteStats> {
if (config.sortByScore) {
let sortedSearch = this.trySortedSearchFromCache(config);
if (!sortedSearch) {
const engineConfig = config.maxResults ?
{
...config,
...{ maxResults: null }
} :
config;
const engine = new FileIndexSearchEngine(engineConfig, provider);
sortedSearch = this.doSortedSearch(engine, config);
}
return new TPromise<ISearchCompleteStats>((c, e) => {
sortedSearch.then(complete => {
this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE);
c(complete);
}, e);
}, () => {
sortedSearch.cancel();
});
}
const engine = new FileIndexSearchEngine(config, provider);
return this.doSearch(engine)
.then(complete => {
this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE);
return <ISearchCompleteStats>{
limitHit: complete.limitHit,
stats: {
type: 'fileIndexProver',
detailStats: complete.stats,
fromCache: false,
resultCount: complete.results.length
}
};
});
}
private getFolderCacheKey(config: ISearchQuery): string {
const uri = config.folderQueries[0].folder.toString();
const folderCacheKey = config.cacheKey && `${uri}_${config.cacheKey}`;
if (!this.folderCacheKeys.get(config.cacheKey)) {
this.folderCacheKeys.set(config.cacheKey, new Set());
}
this.folderCacheKeys.get(config.cacheKey).add(folderCacheKey);
return folderCacheKey;
}
private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch {
return {
resource: match.original || resources.joinPath(match.base, match.relativePath)
};
}
private doSortedSearch(engine: FileIndexSearchEngine, config: ISearchQuery): TPromise<IInternalSearchComplete> {
let searchPromise: TPromise<void>;
let allResultsPromise = new TPromise<IInternalSearchComplete<IFileIndexProviderStats>>((c, e) => {
searchPromise = this.doSearch(engine).then(c, e);
}, () => {
searchPromise.cancel();
});
const folderCacheKey = this.getFolderCacheKey(config);
let cache: Cache;
if (folderCacheKey) {
cache = this.getOrCreateCache(folderCacheKey);
const cacheRow: ICacheRow = {
promise: allResultsPromise,
resolved: false
};
cache.resultsToSearchCache[config.filePattern] = cacheRow;
allResultsPromise.then(() => {
cacheRow.resolved = true;
}, err => {
delete cache.resultsToSearchCache[config.filePattern];
});
allResultsPromise = this.preventCancellation(allResultsPromise);
}
let chained: TPromise<void>;
return new TPromise<IInternalSearchComplete>((c, e) => {
chained = allResultsPromise.then(complete => {
const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
const sortSW = (typeof config.maxResults !== 'number' || config.maxResults > 0) && StopWatch.create();
return this.sortResults(config, complete.results, scorerCache)
.then(sortedResults => {
// sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickopen is opened.
// Contrasting with findFiles which is not sorted and will have sortingTime: undefined
const sortingTime = sortSW ? sortSW.elapsed() : -1;
c(<IInternalSearchComplete>{
limitHit: complete.limitHit || typeof config.maxResults === 'number' && complete.results.length > config.maxResults, // ??
results: sortedResults,
stats: {
detailStats: complete.stats,
fromCache: false,
resultCount: sortedResults.length,
sortingTime,
type: 'fileIndexProver'
}
});
});
}, e);
}, () => {
chained.cancel();
});
}
private getOrCreateCache(cacheKey: string): Cache {
const existing = this.caches[cacheKey];
if (existing) {
return existing;
}
return this.caches[cacheKey] = new Cache();
}
private trySortedSearchFromCache(config: ISearchQuery): TPromise<IInternalSearchComplete> {
const folderCacheKey = this.getFolderCacheKey(config);
const cache = folderCacheKey && this.caches[folderCacheKey];
if (!cache) {
return undefined;
}
const cached = this.getResultsFromCache(cache, config.filePattern);
if (cached) {
let chained: TPromise<void>;
return new TPromise<IInternalSearchComplete>((c, e) => {
chained = cached.then(complete => {
const sortSW = StopWatch.create();
return this.sortResults(config, complete.results, cache.scorerCache)
.then(sortedResults => {
c(<IInternalSearchComplete<IFileSearchStats>>{
limitHit: complete.limitHit || typeof config.maxResults === 'number' && complete.results.length > config.maxResults,
results: sortedResults,
stats: {
fromCache: true,
detailStats: complete.stats,
type: 'fileIndexProver',
resultCount: sortedResults.length,
sortingTime: sortSW.elapsed()
}
});
});
}, e);
}, () => {
chained.cancel();
});
}
return undefined;
}
private sortResults(config: IRawSearchQuery, results: IInternalFileMatch[], scorerCache: ScorerCache): TPromise<IInternalFileMatch[]> {
// we use the same compare function that is used later when showing the results using fuzzy scoring
// this is very important because we are also limiting the number of results by config.maxResults
// and as such we want the top items to be included in this result set if the number of items
// exceeds config.maxResults.
const query = prepareQuery(config.filePattern);
const compare = (matchA: IInternalFileMatch, matchB: IInternalFileMatch) => compareItemsByScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache);
return arrays.topAsync(results, compare, config.maxResults, 10000);
}
private sendAsBatches(rawMatches: IInternalFileMatch[], onBatch: (batch: IFileMatch[]) => void, batchSize: number) {
const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
if (batchSize && batchSize > 0) {
for (let i = 0; i < serializedMatches.length; i += batchSize) {
onBatch(serializedMatches.slice(i, i + batchSize));
}
} else {
onBatch(serializedMatches);
}
}
private getResultsFromCache(cache: Cache, searchValue: string): TPromise<IInternalSearchComplete<ICachedSearchStats>> {
const cacheLookupSW = StopWatch.create();
if (path.isAbsolute(searchValue)) {
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
}
// Find cache entries by prefix of search value
const hasPathSep = searchValue.indexOf(path.sep) >= 0;
let cacheRow: ICacheRow;
for (let previousSearch in cache.resultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (strings.startsWith(searchValue, previousSearch)) {
if (hasPathSep && previousSearch.indexOf(path.sep) < 0) {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
const row = cache.resultsToSearchCache[previousSearch];
cacheRow = {
promise: this.preventCancellation(row.promise),
resolved: row.resolved
};
break;
}
}
if (!cacheRow) {
return null;
}
const cacheLookupTime = cacheLookupSW.elapsed();
const cacheFilterSW = StopWatch.create();
return new TPromise<IInternalSearchComplete<ICachedSearchStats>>((c, e) => {
cacheRow.promise.then(complete => {
// Pattern match on results
let results: IInternalFileMatch[] = [];
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
for (let i = 0; i < complete.results.length; i++) {
let entry = complete.results[i];
// Check if this entry is a match for the search value
if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) {
continue;
}
results.push(entry);
}
c(<IInternalSearchComplete<ICachedSearchStats>>{
limitHit: complete.limitHit,
results,
stats: {
cacheWasResolved: cacheRow.resolved,
cacheLookupTime,
cacheFilterTime: cacheFilterSW.elapsed(),
cacheEntryCount: complete.results.length
}
});
}, e);
}, () => {
cacheRow.promise.cancel();
});
}
private doSearch(engine: FileIndexSearchEngine): TPromise<IInternalSearchComplete<IFileIndexProviderStats>> {
const results: IInternalFileMatch[] = [];
const onResult = match => results.push(match);
return new TPromise<IInternalSearchComplete<IFileIndexProviderStats>>((c, e) => {
engine.search(onResult).then(result => {
c(<IInternalSearchComplete<IFileIndexProviderStats>>{
limitHit: result.isLimitHit,
results,
stats: result.stats
});
}, e);
}, () => {
engine.cancel();
});
}
public clearCache(cacheKey: string): TPromise<void> {
if (!this.folderCacheKeys.has(cacheKey)) {
return TPromise.wrap(undefined);
}
const expandedKeys = this.folderCacheKeys.get(cacheKey);
expandedKeys.forEach(key => delete this.caches[key]);
this.folderCacheKeys.delete(cacheKey);
return TPromise.as(undefined);
}
private preventCancellation<C>(promise: TPromise<C>): TPromise<C> {
return new TPromise<C>((c, e) => {
// Allow for piled up cancellations to come through first.
process.nextTick(() => {
promise.then(c, e);
});
}, () => {
// Do not propagate.
});
}
}
interface ICacheRow {
promise: TPromise<IInternalSearchComplete<IFileIndexProviderStats>>;
resolved: boolean;
}
class Cache {
public resultsToSearchCache: { [searchValue: string]: ICacheRow; } = Object.create(null);
public scorerCache: ScorerCache = Object.create(null);
}
const FileMatchItemAccessor = new class implements IItemAccessor<IInternalFileMatch> {
public getItemLabel(match: IInternalFileMatch): string {
return match.basename; // e.g. myFile.txt
}
public getItemDescription(match: IInternalFileMatch): string {
return match.relativePath.substr(0, match.relativePath.length - match.basename.length - 1); // e.g. some/path/to/file
}
public getItemPath(match: IInternalFileMatch): string {
return match.relativePath; // e.g. some/path/to/file/myFile.txt
}
};

View File

@@ -5,17 +5,20 @@
'use strict';
import * as path from 'path';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import * as glob from 'vs/base/common/glob';
import { toDisposable } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { StopWatch } from 'vs/base/common/stopwatch';
import URI, { UriComponents } from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import * as extfs from 'vs/base/node/extfs';
import { IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery } from 'vs/platform/search/common/search';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, ITextSearchResult } from 'vs/platform/search/common/search';
import { FileIndexSearchManager, IDirectoryEntry, IDirectoryTree, IInternalFileMatch, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/api/node/extHostSearch.fileIndex';
import * as vscode from 'vscode';
import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol';
import { toDisposable } from 'vs/base/common/lifecycle';
export interface ISchemeTransformer {
transformOutgoing(scheme: string): string;
@@ -24,14 +27,18 @@ export interface ISchemeTransformer {
export class ExtHostSearch implements ExtHostSearchShape {
private readonly _proxy: MainThreadSearchShape;
private readonly _searchProvider = new Map<number, vscode.SearchProvider>();
private readonly _fileSearchProvider = new Map<number, vscode.FileSearchProvider>();
private readonly _textSearchProvider = new Map<number, vscode.TextSearchProvider>();
private readonly _fileIndexProvider = new Map<number, vscode.FileIndexProvider>();
private _handlePool: number = 0;
private _fileSearchManager: FileSearchManager;
private _fileIndexSearchManager: FileIndexSearchManager;
constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer, private _extfs = extfs) {
this._proxy = mainContext.getProxy(MainContext.MainThreadSearch);
this._fileSearchManager = new FileSearchManager();
this._fileIndexSearchManager = new FileIndexSearchManager();
}
private _transformScheme(scheme: string): string {
@@ -41,40 +48,80 @@ export class ExtHostSearch implements ExtHostSearchShape {
return scheme;
}
registerSearchProvider(scheme: string, provider: vscode.SearchProvider) {
registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider) {
const handle = this._handlePool++;
this._searchProvider.set(handle, provider);
this._proxy.$registerSearchProvider(handle, this._transformScheme(scheme));
this._fileSearchProvider.set(handle, provider);
this._proxy.$registerFileSearchProvider(handle, this._transformScheme(scheme));
return toDisposable(() => {
this._searchProvider.delete(handle);
this._fileSearchProvider.delete(handle);
this._proxy.$unregisterProvider(handle);
});
}
$provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery): TPromise<ISearchCompleteStats> {
const provider = this._searchProvider.get(handle);
if (!provider.provideFileSearchResults) {
return TPromise.as(undefined);
}
const query = reviveQuery(rawQuery);
return this._fileSearchManager.fileSearch(query, provider, progress => {
this._proxy.$handleFileMatch(handle, session, progress.map(p => p.resource));
registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider) {
const handle = this._handlePool++;
this._textSearchProvider.set(handle, provider);
this._proxy.$registerTextSearchProvider(handle, this._transformScheme(scheme));
return toDisposable(() => {
this._textSearchProvider.delete(handle);
this._proxy.$unregisterProvider(handle);
});
}
$clearCache(handle: number, cacheKey: string): TPromise<void> {
const provider = this._searchProvider.get(handle);
if (!provider.clearCache) {
return TPromise.as(undefined);
}
registerFileIndexProvider(scheme: string, provider: vscode.FileIndexProvider) {
const handle = this._handlePool++;
this._fileIndexProvider.set(handle, provider);
this._proxy.$registerFileIndexProvider(handle, this._transformScheme(scheme));
return toDisposable(() => {
this._fileSearchProvider.delete(handle);
this._proxy.$unregisterProvider(handle); // TODO@roblou - unregisterFileIndexProvider
});
}
return TPromise.as(
this._fileSearchManager.clearCache(cacheKey, provider));
$provideFileSearchResults(handle: number, session: number, rawQuery: IRawSearchQuery): TPromise<ISearchCompleteStats> {
const provider = this._fileSearchProvider.get(handle);
const query = reviveQuery(rawQuery);
if (provider) {
let cancelSource = new CancellationTokenSource();
return new TPromise((c, e) => {
this._fileSearchManager.fileSearch(query, provider, batch => {
this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource));
}, cancelSource.token).then(c, err => {
if (!isPromiseCanceledError(err)) {
e(err);
}
});
}, () => {
// TODO IPC promise cancellation #53526
cancelSource.cancel();
cancelSource.dispose();
});
} else {
const indexProvider = this._fileIndexProvider.get(handle);
if (indexProvider) {
return this._fileIndexSearchManager.fileSearch(query, indexProvider, batch => {
this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource));
}).then(null, err => {
if (!isPromiseCanceledError(err)) {
throw err;
}
return null;
});
} else {
throw new Error('something went wrong');
}
}
}
$clearCache(cacheKey: string): TPromise<void> {
// Actually called once per provider.
// Only relevant to file index search.
return this._fileIndexSearchManager.clearCache(cacheKey);
}
$provideTextSearchResults(handle: number, session: number, pattern: IPatternInfo, rawQuery: IRawSearchQuery): TPromise<ISearchCompleteStats> {
const provider = this._searchProvider.get(handle);
const provider = this._textSearchProvider.get(handle);
if (!provider.provideTextSearchResults) {
return TPromise.as(undefined);
}
@@ -85,22 +132,6 @@ export class ExtHostSearch implements ExtHostSearchShape {
}
}
/**
* Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
*/
function resolvePatternsForProvider(globalPattern: glob.IExpression, folderPattern: glob.IExpression): string[] {
const merged = {
...(globalPattern || {}),
...(folderPattern || {})
};
return Object.keys(merged)
.filter(key => {
const value = merged[key];
return typeof value === 'boolean' && value;
});
}
function reviveQuery(rawQuery: IRawSearchQuery): ISearchQuery {
return {
...rawQuery,
@@ -141,22 +172,16 @@ class TextSearchResultsCollector {
if (!this._currentFileMatch) {
this._currentFileMatch = {
resource: data.uri,
lineMatches: []
matches: []
};
}
// TODO@roblou - line text is sent for every match
const matchRange = data.preview.match;
this._currentFileMatch.lineMatches.push({
lineNumber: data.range.start.line,
preview: data.preview.text,
offsetAndLengths: [[matchRange.start.character, matchRange.end.character - matchRange.start.character]]
});
this._currentFileMatch.matches.push(extensionResultToFrontendResult(data));
}
private pushToCollector(): void {
const size = this._currentFileMatch ?
this._currentFileMatch.lineMatches.reduce((acc, match) => acc + match.offsetAndLengths.length, 0) :
this._currentFileMatch.matches.length :
0;
this._batchedCollector.addItem(this._currentFileMatch, size);
}
@@ -171,6 +196,26 @@ class TextSearchResultsCollector {
}
}
function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult {
return {
preview: {
match: {
startLineNumber: data.preview.match.start.line,
startColumn: data.preview.match.start.character,
endLineNumber: data.preview.match.end.line,
endColumn: data.preview.match.end.character
},
text: data.preview.text
},
range: {
startLineNumber: data.range.start.line,
startColumn: data.range.start.character,
endLineNumber: data.range.end.line,
endColumn: data.range.end.character
}
};
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
@@ -253,107 +298,6 @@ class BatchedCollector<T> {
}
}
interface IDirectoryEntry {
base: URI;
relativePath: string;
basename: string;
}
interface IDirectoryTree {
rootEntries: IDirectoryEntry[];
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}
interface IInternalFileMatch {
base: URI;
relativePath?: string; // Not present for extraFiles or absolute path matches
basename: string;
size?: number;
}
class QueryGlobTester {
private _excludeExpression: glob.IExpression;
private _parsedExcludeExpression: glob.ParsedExpression;
private _parsedIncludeExpression: glob.ParsedExpression;
constructor(config: ISearchQuery, folderQuery: IFolderQuery) {
this._excludeExpression = {
...(config.excludePattern || {}),
...(folderQuery.excludePattern || {})
};
this._parsedExcludeExpression = glob.parse(this._excludeExpression);
// Empty includeExpression means include nothing, so no {} shortcuts
let includeExpression: glob.IExpression = config.includePattern;
if (folderQuery.includePattern) {
if (includeExpression) {
includeExpression = {
...includeExpression,
...folderQuery.includePattern
};
} else {
includeExpression = folderQuery.includePattern;
}
}
if (includeExpression) {
this._parsedIncludeExpression = glob.parse(includeExpression);
}
}
/**
* Guaranteed sync - siblingsFn should not return a promise.
*/
public includedInQuerySync(testPath: string, basename?: string, hasSibling?: (name: string) => boolean): boolean {
if (this._parsedExcludeExpression && this._parsedExcludeExpression(testPath, basename, hasSibling)) {
return false;
}
if (this._parsedIncludeExpression && !this._parsedIncludeExpression(testPath, basename, hasSibling)) {
return false;
}
return true;
}
/**
* Guaranteed async.
*/
public includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | TPromise<boolean>): TPromise<boolean> {
const excludeP = this._parsedExcludeExpression ?
TPromise.as(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
TPromise.wrap(false);
return excludeP.then(excluded => {
if (excluded) {
return false;
}
return this._parsedIncludeExpression ?
TPromise.as(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) :
TPromise.wrap(true);
}).then(included => {
return included;
});
}
public hasSiblingExcludeClauses(): boolean {
return hasSiblingClauses(this._excludeExpression);
}
}
function hasSiblingClauses(pattern: glob.IExpression): boolean {
for (let key in pattern) {
if (typeof pattern[key] !== 'boolean') {
return true;
}
}
return false;
}
class TextSearchEngine {
private activeCancellationTokens = new Set<CancellationTokenSource>();
@@ -363,7 +307,7 @@ class TextSearchEngine {
private resultCount = 0;
private isCanceled: boolean;
constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.SearchProvider, private _extfs: typeof extfs) {
constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs) {
}
public cancel(): void {
@@ -372,10 +316,10 @@ class TextSearchEngine {
this.activeCancellationTokens = new Set();
}
public search(onProgress: (matches: IFileMatch[]) => void): TPromise<{ limitHit: boolean }> {
public search(onProgress: (matches: IFileMatch[]) => void): TPromise<ISearchCompleteStats> {
const folderQueries = this.config.folderQueries;
return new TPromise<{ limitHit: boolean }>((resolve, reject) => {
return new TPromise<ISearchCompleteStats>((resolve, reject) => {
this.collector = new TextSearchResultsCollector(onProgress);
const onResult = (match: vscode.TextSearchResult, folderIdx: number) => {
@@ -399,7 +343,12 @@ class TextSearchEngine {
return this.searchInFolder(fq, r => onResult(r, i));
})).then(() => {
this.collector.flush();
resolve({ limitHit: this.isLimitHit });
resolve({
limitHit: this.isLimitHit,
stats: {
type: 'textSearchProvider'
}
});
}, (errs: Error[]) => {
const errMsg = errs
.map(err => toErrorMessage(err))
@@ -479,7 +428,8 @@ class TextSearchEngine {
followSymlinks: !this.config.ignoreSymlinks,
encoding: this.config.fileEncoding,
maxFileSize: this.config.maxFileSize,
maxResults: this.config.maxResults
maxResults: this.config.maxResults,
previewOptions: this.config.previewOptions
};
}
}
@@ -506,7 +456,7 @@ class FileSearchEngine {
private globalExcludePattern: glob.ParsedExpression;
constructor(private config: ISearchQuery, private provider: vscode.SearchProvider) {
constructor(private config: ISearchQuery, private provider: vscode.FileSearchProvider) {
this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || null;
@@ -535,7 +485,7 @@ class FileSearchEngine {
// Support that the file pattern is a full path to a file that exists
if (this.isCanceled) {
return resolve({ limitHit: this.isLimitHit, cacheKeys: [] });
return resolve({ limitHit: this.isLimitHit });
}
// For each extra file
@@ -556,8 +506,11 @@ class FileSearchEngine {
// For each root folder
TPromise.join(folderQueries.map(fq => {
return this.searchInFolder(fq, onResult);
})).then(cacheKeys => {
resolve({ limitHit: this.isLimitHit, cacheKeys });
})).then(stats => {
resolve({
limitHit: this.isLimitHit,
stats: stats[0] // Only looking at single-folder workspace stats...
});
}, (errs: Error[]) => {
const errMsg = errs
.map(err => toErrorMessage(err))
@@ -568,7 +521,7 @@ class FileSearchEngine {
});
}
private searchInFolder(fq: IFolderQuery<URI>, onResult: (match: IInternalFileMatch) => void): TPromise<string> {
private searchInFolder(fq: IFolderQuery<URI>, onResult: (match: IInternalFileMatch) => void): TPromise<IFileSearchProviderStats> {
let cancellation = new CancellationTokenSource();
return new TPromise((resolve, reject) => {
const options = this.getSearchOptionsForFolder(fq);
@@ -577,52 +530,57 @@ class FileSearchEngine {
const queryTester = new QueryGlobTester(this.config, fq);
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
const onProviderResult = (result: URI) => {
if (this.isCanceled) {
return;
}
const relativePath = path.relative(fq.folder.fsPath, result.fsPath);
if (noSiblingsClauses) {
const basename = path.basename(result.fsPath);
this.matchFile(onResult, { base: fq.folder, relativePath, basename });
return;
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(tree, fq.folder, relativePath, onResult);
};
let folderCacheKey: string;
let providerSW: StopWatch;
new TPromise(_resolve => process.nextTick(_resolve))
.then(() => {
this.activeCancellationTokens.add(cancellation);
folderCacheKey = this.config.cacheKey && (this.config.cacheKey + '_' + fq.folder.fsPath);
providerSW = StopWatch.create();
return this.provider.provideFileSearchResults(
{
pattern: this.config.filePattern || '',
cacheKey: folderCacheKey
pattern: this.config.filePattern || ''
},
options,
{ report: onProviderResult },
cancellation.token);
})
.then(() => {
.then(results => {
const providerTime = providerSW.elapsed();
const postProcessSW = StopWatch.create();
if (this.isCanceled) {
return null;
}
if (results) {
results.forEach(result => {
const relativePath = path.relative(fq.folder.fsPath, result.fsPath);
if (noSiblingsClauses) {
const basename = path.basename(result.fsPath);
this.matchFile(onResult, { base: fq.folder, relativePath, basename });
return;
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(tree, fq.folder, relativePath, onResult);
});
}
this.activeCancellationTokens.delete(cancellation);
if (this.isCanceled) {
return null;
}
this.matchDirectoryTree(tree, queryTester, onResult);
return null;
return <IFileSearchProviderStats>{
providerTime,
postProcessTime: postProcessSW.elapsed()
};
}).then(
() => {
stats => {
cancellation.dispose();
resolve(folderCacheKey);
resolve(stats);
},
err => {
cancellation.dispose();
@@ -731,85 +689,77 @@ class FileSearchEngine {
interface IInternalSearchComplete {
limitHit: boolean;
cacheKeys: string[];
stats?: IFileSearchProviderStats;
}
class FileSearchManager {
private static readonly BATCH_SIZE = 512;
private readonly expandedCacheKeys = new Map<string, string[]>();
fileSearch(config: ISearchQuery, provider: vscode.FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): TPromise<ISearchCompleteStats> {
const engine = new FileSearchEngine(config, provider);
fileSearch(config: ISearchQuery, provider: vscode.SearchProvider, onResult: (matches: IFileMatch[]) => void): TPromise<ISearchCompleteStats> {
let searchP: TPromise;
return new TPromise<ISearchCompleteStats>((c, e) => {
const engine = new FileSearchEngine(config, provider);
let resultCount = 0;
const onInternalResult = (batch: IInternalFileMatch[]) => {
resultCount += batch.length;
onBatch(batch.map(m => this.rawMatchToSearchItem(m)));
};
const onInternalResult = (progress: IInternalFileMatch[]) => {
onResult(progress.map(m => this.rawMatchToSearchItem(m)));
};
searchP = this.doSearch(engine, FileSearchManager.BATCH_SIZE, onInternalResult).then(
result => {
if (config.cacheKey) {
this.expandedCacheKeys.set(config.cacheKey, result.cacheKeys);
return this.doSearch(engine, FileSearchManager.BATCH_SIZE, onInternalResult, token).then(
result => {
return <ISearchCompleteStats>{
limitHit: result.limitHit,
stats: {
fromCache: false,
type: 'fileSearchProvider',
resultCount,
detailStats: result.stats
}
c({
limitHit: result.limitHit
});
},
e);
}, () => {
if (searchP) {
searchP.cancel();
}
});
}
clearCache(cacheKey: string, provider: vscode.SearchProvider): void {
if (!this.expandedCacheKeys.has(cacheKey)) {
return;
}
this.expandedCacheKeys.get(cacheKey).forEach(key => provider.clearCache(key));
this.expandedCacheKeys.delete(cacheKey);
};
});
}
private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch {
return {
resource: resources.joinPath(match.base, match.relativePath)
};
if (match.relativePath) {
return {
resource: resources.joinPath(match.base, match.relativePath)
};
} else {
// extraFileResources
return {
resource: match.base
};
}
}
private doSearch(engine: FileSearchEngine, batchSize: number, onResultBatch: (matches: IInternalFileMatch[]) => void): TPromise<IInternalSearchComplete> {
return new TPromise((c, e) => {
const _onResult = match => {
if (match) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
onResultBatch(batch);
batch = [];
}
}
};
let batch: IInternalFileMatch[] = [];
engine.search(_onResult).then(result => {
if (batch.length) {
onResultBatch(batch);
}
c(result);
}, error => {
if (batch.length) {
onResultBatch(batch);
}
e(error);
});
}, () => {
private doSearch(engine: FileSearchEngine, batchSize: number, onResultBatch: (matches: IInternalFileMatch[]) => void, token: CancellationToken): TPromise<IInternalSearchComplete> {
token.onCancellationRequested(() => {
engine.cancel();
});
const _onResult = match => {
if (match) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
onResultBatch(batch);
batch = [];
}
}
};
let batch: IInternalFileMatch[] = [];
return engine.search(_onResult).then(result => {
if (batch.length) {
onResultBatch(batch);
}
return result;
}, error => {
if (batch.length) {
onResultBatch(batch);
}
return TPromise.wrapError(error);
});
}
}

View File

@@ -497,6 +497,11 @@ export class ExtHostTextEditor implements vscode.TextEditor {
private _applyEdit(editBuilder: TextEditorEdit): TPromise<boolean> {
let editData = editBuilder.finalize();
// return when there is nothing to do
if (editData.edits.length === 0 && !editData.setEndOfLine) {
return TPromise.wrap(true);
}
// check that the edits are not overlapping (i.e. illegal)
let editRanges = editData.edits.map(edit => edit.range);

View File

@@ -1956,3 +1956,10 @@ export enum CommentThreadCollapsibleState {
*/
Expanded = 1
}
export class QuickInputButtons {
static readonly Back: vscode.QuickInputButton = { iconPath: 'back.svg' };
private constructor() { }
}

View File

@@ -3,14 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol';
import * as vscode from 'vscode';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { EditorViewColumn } from 'vs/workbench/api/shared/editor';
import { TPromise } from 'vs/base/common/winjs.base';
import * as vscode from 'vscode';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol';
import { Disposable } from './extHostTypes';
import URI from 'vs/base/common/uri';
type IconPath = URI | { light: URI, dark: URI };
export class ExtHostWebview implements vscode.Webview {
private readonly _handle: WebviewPanelHandle;
@@ -78,6 +80,7 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel {
private readonly _proxy: MainThreadWebviewsShape;
private readonly _viewType: string;
private _title: string;
private _iconPath: IconPath;
private readonly _options: vscode.WebviewPanelOptions;
private readonly _webview: ExtHostWebview;
@@ -150,6 +153,20 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel {
}
}
get iconPath(): IconPath | undefined {
this.assertNotDisposed();
return this._iconPath;
}
set iconPath(value: IconPath | undefined) {
this.assertNotDisposed();
if (this._iconPath !== value) {
this._iconPath = value;
this._proxy.$setIconPath(this._handle, URI.isUri(value) ? { light: value, dark: value } : value);
}
}
get options() {
return this._options;
}
@@ -191,9 +208,10 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel {
public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void {
this.assertNotDisposed();
this._proxy.$reveal(this._handle,
viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined,
!!preserveFocus);
this._proxy.$reveal(this._handle, {
viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined,
preserveFocus: !!preserveFocus
});
}
private assertNotDisposed() {
@@ -206,8 +224,11 @@ export class ExtHostWebviewPanel implements vscode.WebviewPanel {
export class ExtHostWebviews implements ExtHostWebviewsShape {
private static webviewHandlePool = 1;
private readonly _proxy: MainThreadWebviewsShape;
private static newHandle(): WebviewPanelHandle {
return ExtHostWebviews.webviewHandlePool++ + '';
}
private readonly _proxy: MainThreadWebviewsShape;
private readonly _webviewPanels = new Map<WebviewPanelHandle, ExtHostWebviewPanel>();
private readonly _serializers = new Map<string, vscode.WebviewPanelSerializer>();
@@ -217,22 +238,20 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews);
}
createWebview(
public createWebview(
extensionLocation: URI,
viewType: string,
title: string,
showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean },
options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) | undefined,
extensionLocation: URI
options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {},
): vscode.WebviewPanel {
options = options || {};
const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions;
const webviewShowOptions = {
viewColumn: typeConverters.ViewColumn.from(viewColumn),
preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus
};
const handle = ExtHostWebviews.webviewHandlePool++ + '';
const handle = ExtHostWebviews.newHandle();
this._proxy.$createWebviewPanel(handle, viewType, title, webviewShowOptions, options, extensionLocation);
const webview = new ExtHostWebview(handle, this._proxy, options);
@@ -241,7 +260,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
return panel;
}
registerWebviewPanelSerializer(
public registerWebviewPanelSerializer(
viewType: string,
serializer: vscode.WebviewPanelSerializer
): vscode.Disposable {
@@ -258,14 +277,20 @@ export class ExtHostWebviews implements ExtHostWebviewsShape {
});
}
$onMessage(handle: WebviewPanelHandle, message: any): void {
public $onMessage(
handle: WebviewPanelHandle,
message: any
): void {
const panel = this.getWebviewPanel(handle);
if (panel) {
panel.webview._onMessageEmitter.fire(message);
}
}
$onDidChangeWebviewPanelViewState(handle: WebviewPanelHandle, newState: WebviewPanelViewState): void {
public $onDidChangeWebviewPanelViewState(
handle: WebviewPanelHandle,
newState: WebviewPanelViewState
): void {
const panel = this.getWebviewPanel(handle);
if (panel) {
const viewColumn = typeConverters.ViewColumn.to(newState.position);

View File

@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { posix, relative, join } from 'path';
import { join, relative } from 'path';
import { delta as arrayDelta } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { TernarySearchTree } from 'vs/base/common/map';
import { Counter } from 'vs/base/common/numbers';
import { normalize } from 'vs/base/common/paths';
import { isLinux } from 'vs/base/common/platform';
import { basenameOrAuthority, isEqual } from 'vs/base/common/resources';
import { basenameOrAuthority, dirname, 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';
@@ -107,8 +108,8 @@ class ExtHostWorkspaceImpl extends Workspace {
private readonly _workspaceFolders: vscode.WorkspaceFolder[] = [];
private readonly _structure = TernarySearchTree.forPaths<vscode.WorkspaceFolder>();
private constructor(id: string, name: string, folders: vscode.WorkspaceFolder[]) {
super(id, name, folders.map(f => new WorkspaceFolder(f)));
private constructor(id: string, private _name: string, folders: vscode.WorkspaceFolder[]) {
super(id, folders.map(f => new WorkspaceFolder(f)));
// setup the workspace folder data structure
folders.forEach(folder => {
@@ -117,6 +118,10 @@ class ExtHostWorkspaceImpl extends Workspace {
});
}
get name(): string {
return this._name;
}
get workspaceFolders(): vscode.WorkspaceFolder[] {
return this._workspaceFolders.slice(0);
}
@@ -124,7 +129,7 @@ class ExtHostWorkspaceImpl extends Workspace {
getWorkspaceFolder(uri: URI, resolveParent?: boolean): vscode.WorkspaceFolder {
if (resolveParent && this._structure.get(uri.toString())) {
// `uri` is a workspace folder so we check for its parent
uri = uri.with({ path: posix.dirname(uri.path) });
uri = dirname(uri);
}
return this._structure.findSubstr(uri.toString());
}
@@ -136,8 +141,6 @@ class ExtHostWorkspaceImpl extends Workspace {
export class ExtHostWorkspace implements ExtHostWorkspaceShape {
private static _requestIdPool = 0;
private readonly _onDidChangeWorkspace = new Emitter<vscode.WorkspaceFoldersChangeEvent>();
private readonly _proxy: MainThreadWorkspaceShape;
@@ -148,12 +151,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
readonly onDidChangeWorkspace: Event<vscode.WorkspaceFoldersChangeEvent> = this._onDidChangeWorkspace.event;
private readonly _activeSearchCallbacks = [];
private readonly _activeSearchCallbacks: ((match: IRawFileMatch2) => any)[] = [];
constructor(
mainContext: IMainContext,
data: IWorkspaceData,
private _logService: ILogService
private _logService: ILogService,
private _requestIdProvider: Counter
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadWorkspace);
this._messageService = mainContext.getProxy(MainContext.MainThreadMessageService);
@@ -166,6 +170,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
return this._actualWorkspace;
}
get name(): string {
return this._actualWorkspace ? this._actualWorkspace.name : undefined;
}
private get _actualWorkspace(): ExtHostWorkspaceImpl {
return this._unconfirmedWorkspace || this._confirmedWorkspace;
}
@@ -340,7 +348,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
findFiles(include: vscode.GlobPattern, exclude: vscode.GlobPattern, maxResults: number, extensionId: string, token?: vscode.CancellationToken): Thenable<vscode.Uri[]> {
this._logService.trace(`extHostWorkspace#findFiles: fileSearch, extension: ${extensionId}, entryPoint: findFiles`);
const requestId = ExtHostWorkspace._requestIdPool++;
const requestId = this._requestIdProvider.getNext();
let includePattern: string;
let includeFolder: string;
@@ -374,7 +382,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
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 requestId = this._requestIdProvider.getNext();
const globPatternToString = (pattern: vscode.GlobPattern | string) => {
if (typeof pattern === 'string') {
@@ -390,26 +398,36 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
disregardExcludeSettings: options.exclude === null,
fileEncoding: options.encoding,
maxResults: options.maxResults,
previewOptions: options.previewOptions,
includePattern: options.include && globPatternToString(options.include),
excludePattern: options.exclude && globPatternToString(options.exclude)
};
let isCanceled = false;
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({
uri: URI.revive(p.resource),
preview: { text: lineMatch.preview, match: range },
range
});
if (isCanceled) {
return;
}
p.matches.forEach(match => {
callback({
uri: URI.revive(p.resource),
preview: {
text: match.preview.text,
match: new Range(match.preview.match.startLineNumber, match.preview.match.startColumn, match.preview.match.endLineNumber, match.preview.match.endColumn)
},
range: new Range(match.range.startLineNumber, match.range.startColumn, match.range.endLineNumber, match.range.endColumn)
});
});
};
if (token) {
token.onCancellationRequested(() => this._proxy.$cancelSearch(requestId));
token.onCancellationRequested(() => {
isCanceled = true;
this._proxy.$cancelSearch(requestId);
});
}
return this._proxy.$startTextSearch(query, queryOptions, requestId).then(