mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 02:28:34 +01:00
Merge branch 'master' into aeschli/recentWithLabel
This commit is contained in:
@@ -119,7 +119,7 @@ export function createApiFactory(
|
||||
const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService));
|
||||
const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments));
|
||||
const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol, schemeTransformer, extHostLogService));
|
||||
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace, extHostDocumentsAndEditors, extHostConfiguration));
|
||||
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace, extHostDocumentsAndEditors, extHostConfiguration, extHostTerminalService));
|
||||
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
|
||||
rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService);
|
||||
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
|
||||
@@ -527,7 +527,7 @@ export function createApiFactory(
|
||||
onDidChangeWorkspaceFolders: function (listener, thisArgs?, disposables?) {
|
||||
return extHostWorkspace.onDidChangeWorkspace(listener, thisArgs, disposables);
|
||||
},
|
||||
asRelativePath: (pathOrUri, includeWorkspace) => {
|
||||
asRelativePath: (pathOrUri, includeWorkspace?) => {
|
||||
return extHostWorkspace.getRelativePath(pathOrUri, includeWorkspace);
|
||||
},
|
||||
findFiles: (include, exclude, maxResults?, token?) => {
|
||||
@@ -624,9 +624,6 @@ export function createApiFactory(
|
||||
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 extHostComment.registerDocumentCommentProvider(extension.identifier, provider);
|
||||
}),
|
||||
@@ -658,8 +655,8 @@ export function createApiFactory(
|
||||
};
|
||||
|
||||
const comment: typeof vscode.comment = {
|
||||
createCommentControl(id: string, label: string) {
|
||||
return extHostComment.createCommentControl(extension, id, label);
|
||||
createCommentController(id: string, label: string) {
|
||||
return extHostComment.createCommentController(extension, id, label);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -780,6 +777,7 @@ export function createApiFactory(
|
||||
DocumentSymbol: extHostTypes.DocumentSymbol,
|
||||
EndOfLine: extHostTypes.EndOfLine,
|
||||
EventEmitter: Emitter,
|
||||
CustomExecution: extHostTypes.CustomExecution,
|
||||
FileChangeType: extHostTypes.FileChangeType,
|
||||
FileSystemError: extHostTypes.FileSystemError,
|
||||
FileType: files.FileType,
|
||||
|
||||
@@ -119,16 +119,16 @@ export interface CommentProviderFeatures {
|
||||
}
|
||||
|
||||
export interface MainThreadCommentsShape extends IDisposable {
|
||||
$registerCommentControl(handle: number, id: string, label: string): void;
|
||||
$createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], commands: modes.Command[], collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined;
|
||||
$registerCommentController(handle: number, id: string, label: string): void;
|
||||
$createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, comments: modes.Comment[], acceptInputCommand: modes.Command, additionalCommands: modes.Command[], collapseState: modes.CommentThreadCollapsibleState): modes.CommentThread2 | undefined;
|
||||
$deleteCommentThread(handle: number, commentThreadHandle: number): void;
|
||||
$updateComments(handle: number, commentThreadHandle: number, comments: modes.Comment[]): void;
|
||||
$createCommentingRanges(handle: number, commentingRangesHandle: number, resource: UriComponents, ranges: IRange[], commands: modes.Command): void;
|
||||
$deleteCommentingRanges(handle: number, commentingRangesHandle: number): void;
|
||||
$updateCommentingRanges(handle: number, commentingRangesHandle: number, newRanges: IRange[]): void;
|
||||
$updateCommentingRangesCommands(handle: number, commentingRangesHandle: number, command: modes.Command): void;
|
||||
$setInputValue(handle: number, commentThreadHandle: number, input: string): void;
|
||||
$updateCommentThreadCommands(handle: number, commentThreadHandle: number, acceptInputCommands: modes.Command[]): void;
|
||||
$setInputValue(handle: number, input: string): void;
|
||||
$updateCommentThreadAcceptInputCommand(handle: number, commentThreadHandle: number, acceptInputCommand: modes.Command): void;
|
||||
$updateCommentThreadAdditionalCommands(handle: number, commentThreadHandle: number, additionalCommands: modes.Command[]): void;
|
||||
$updateCommentThreadCollapsibleState(handle: number, commentThreadHandle: number, collapseState: modes.CommentThreadCollapsibleState): void;
|
||||
$updateCommentThreadRange(handle: number, commentThreadHandle: number, range: IRange): void;
|
||||
$updateCommentThreadLabel(handle: number, commentThreadHandle: number, label: string): void;
|
||||
$registerDocumentCommentProvider(handle: number, features: CommentProviderFeatures): void;
|
||||
$unregisterDocumentCommentProvider(handle: number): void;
|
||||
$registerWorkspaceCommentProvider(handle: number, extensionId: ExtensionIdentifier): void;
|
||||
@@ -224,7 +224,7 @@ export interface ITextDocumentShowOptions {
|
||||
}
|
||||
|
||||
export interface MainThreadTextEditorsShape extends IDisposable {
|
||||
$tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string>;
|
||||
$tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined>;
|
||||
$registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void;
|
||||
$removeTextEditorDecorationType(key: string): void;
|
||||
$tryShowEditor(id: string, position: EditorViewColumn): Promise<void>;
|
||||
@@ -551,7 +551,6 @@ export interface MainThreadFileSystemShape extends IDisposable {
|
||||
export interface MainThreadSearchShape extends IDisposable {
|
||||
$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;
|
||||
@@ -559,12 +558,14 @@ export interface MainThreadSearchShape extends IDisposable {
|
||||
}
|
||||
|
||||
export interface MainThreadTaskShape extends IDisposable {
|
||||
$createTaskId(task: TaskDTO): Promise<string>;
|
||||
$registerTaskProvider(handle: number): Promise<void>;
|
||||
$unregisterTaskProvider(handle: number): Promise<void>;
|
||||
$fetchTasks(filter?: TaskFilterDTO): Promise<TaskDTO[]>;
|
||||
$executeTask(task: TaskHandleDTO | TaskDTO): Promise<TaskExecutionDTO>;
|
||||
$terminateTask(id: string): Promise<void>;
|
||||
$registerTaskSystem(scheme: string, info: TaskSystemInfoDTO): void;
|
||||
$customExecutionComplete(id: string, result?: number): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MainThreadExtensionServiceShape extends IDisposable {
|
||||
@@ -996,7 +997,7 @@ export interface ExtHostSCMShape {
|
||||
|
||||
export interface ExtHostTaskShape {
|
||||
$provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable<TaskSetDTO>;
|
||||
$onDidStartTask(execution: TaskExecutionDTO): void;
|
||||
$onDidStartTask(execution: TaskExecutionDTO, terminalId: number): void;
|
||||
$onDidStartTaskProcess(value: TaskProcessStartedDTO): void;
|
||||
$onDidEndTaskProcess(value: TaskProcessEndedDTO): void;
|
||||
$OnDidEndTask(execution: TaskExecutionDTO): void;
|
||||
@@ -1104,8 +1105,9 @@ export interface ExtHostProgressShape {
|
||||
export interface ExtHostCommentsShape {
|
||||
$provideDocumentComments(handle: number, document: UriComponents): Promise<modes.CommentInfo>;
|
||||
$createNewCommentThread(handle: number, document: UriComponents, range: IRange, text: string): Promise<modes.CommentThread>;
|
||||
$onActiveCommentWidgetChange(commentControlhandle: number, commentThread: modes.CommentThread2, comment: modes.Comment | undefined, input: string): Promise<number | undefined>;
|
||||
$onActiveCommentingRangeChange(commentControlhandle: number, range: IRange): void;
|
||||
$onCommentWidgetInputChange(commentControllerHandle: number, input: string): Promise<number | undefined>;
|
||||
$provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<IRange[] | undefined>;
|
||||
$createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): void;
|
||||
$replyToCommentThread(handle: number, document: UriComponents, range: IRange, commentThread: modes.CommentThread, text: string): Promise<modes.CommentThread>;
|
||||
$editComment(handle: number, document: UriComponents, comment: modes.Comment, text: string): Promise<void>;
|
||||
$deleteComment(handle: number, document: UriComponents, comment: modes.Comment): Promise<void>;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments';
|
||||
import * as extHostTypeConverter from 'vs/workbench/api/node/extHostTypeConverters';
|
||||
import * as types from 'vs/workbench/api/node/extHostTypes';
|
||||
import * as vscode from 'vscode';
|
||||
import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape } from './extHost.protocol';
|
||||
import { CommandsConverter, ExtHostCommands } from './extHostCommands';
|
||||
@@ -30,9 +31,9 @@ export class ExtHostComments implements ExtHostCommentsShape {
|
||||
|
||||
private _proxy: MainThreadCommentsShape;
|
||||
|
||||
private _commentControls: Map<ProviderHandle, ExtHostCommentControl> = new Map<ProviderHandle, ExtHostCommentControl>();
|
||||
private _commentControllers: Map<ProviderHandle, ExtHostCommentController> = new Map<ProviderHandle, ExtHostCommentController>();
|
||||
|
||||
private _commentControlsByExtension: Map<string, ExtHostCommentControl[]> = new Map<string, ExtHostCommentControl[]>();
|
||||
private _commentControllersByExtension: Map<string, ExtHostCommentController[]> = new Map<string, ExtHostCommentController[]>();
|
||||
|
||||
private _documentProviders = new Map<number, HandlerData<vscode.DocumentCommentProvider>>();
|
||||
private _workspaceProviders = new Map<number, HandlerData<vscode.WorkspaceCommentProvider>>();
|
||||
@@ -47,21 +48,21 @@ export class ExtHostComments implements ExtHostCommentsShape {
|
||||
_commands.registerArgumentProcessor({
|
||||
processArgument: arg => {
|
||||
if (arg && arg.$mid === 6) {
|
||||
const commentControl = this._commentControls.get(arg.handle);
|
||||
const commentController = this._commentControllers.get(arg.handle);
|
||||
|
||||
if (!commentControl) {
|
||||
if (!commentController) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
return commentControl;
|
||||
return commentController;
|
||||
} else if (arg && arg.$mid === 7) {
|
||||
const commentControl = this._commentControls.get(arg.commentControlHandle);
|
||||
const commentController = this._commentControllers.get(arg.commentControlHandle);
|
||||
|
||||
if (!commentControl) {
|
||||
if (!commentController) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
const commentThread = commentControl.getCommentThread(arg.commentThreadHandle);
|
||||
const commentThread = commentController.getCommentThread(arg.commentThreadHandle);
|
||||
|
||||
if (!commentThread) {
|
||||
return arg;
|
||||
@@ -75,48 +76,51 @@ export class ExtHostComments implements ExtHostCommentsShape {
|
||||
});
|
||||
}
|
||||
|
||||
createCommentControl(extension: IExtensionDescription, id: string, label: string): vscode.CommentControl {
|
||||
createCommentController(extension: IExtensionDescription, id: string, label: string): vscode.CommentController {
|
||||
const handle = ExtHostComments.handlePool++;
|
||||
const commentControl = new ExtHostCommentControl(extension, handle, this._commands.converter, this._proxy, id, label);
|
||||
this._commentControls.set(commentControl.handle, commentControl);
|
||||
const commentController = new ExtHostCommentController(extension, handle, this._commands.converter, this._proxy, id, label);
|
||||
this._commentControllers.set(commentController.handle, commentController);
|
||||
|
||||
const commentControls = this._commentControlsByExtension.get(ExtensionIdentifier.toKey(extension.identifier)) || [];
|
||||
commentControls.push(commentControl);
|
||||
this._commentControlsByExtension.set(ExtensionIdentifier.toKey(extension.identifier), commentControls);
|
||||
const commentControllers = this._commentControllersByExtension.get(ExtensionIdentifier.toKey(extension.identifier)) || [];
|
||||
commentControllers.push(commentController);
|
||||
this._commentControllersByExtension.set(ExtensionIdentifier.toKey(extension.identifier), commentControllers);
|
||||
|
||||
return commentControl;
|
||||
return commentController;
|
||||
}
|
||||
|
||||
$onActiveCommentWidgetChange(commentControlhandle: number, commentThread: modes.CommentThread2, comment: modes.Comment | undefined, input: string): Promise<number | undefined> {
|
||||
const commentControl = this._commentControls.get(commentControlhandle);
|
||||
$onCommentWidgetInputChange(commentControllerHandle: number, input: string): Promise<number | undefined> {
|
||||
const commentController = this._commentControllers.get(commentControllerHandle);
|
||||
|
||||
if (!commentControl) {
|
||||
if (!commentController) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
commentControl.$onActiveCommentWidgetChange(commentThread, comment, input);
|
||||
return Promise.resolve(commentControlhandle);
|
||||
commentController.$onCommentWidgetInputChange(input);
|
||||
return Promise.resolve(commentControllerHandle);
|
||||
}
|
||||
|
||||
$onCommentWidgetInputChange(commentControlhandle: number, value: string): Promise<void> {
|
||||
const commentControl = this._commentControls.get(commentControlhandle);
|
||||
$provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<IRange[] | undefined> {
|
||||
const commentController = this._commentControllers.get(commentControllerHandle);
|
||||
|
||||
if (!commentControl || !commentControl.widget) {
|
||||
if (!commentController || !commentController.commentingRangeProvider) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
commentControl.widget.input = value;
|
||||
return Promise.resolve(undefined);
|
||||
const document = this._documents.getDocument(URI.revive(uriComponents));
|
||||
return asPromise(() => {
|
||||
return commentController.commentingRangeProvider.provider(document, token);
|
||||
}).then(ranges => ranges ? ranges.map(extHostTypeConverter.Range.from) : undefined);
|
||||
}
|
||||
|
||||
$onActiveCommentingRangeChange(commentControlhandle: number, range: IRange) {
|
||||
const commentControl = this._commentControls.get(commentControlhandle);
|
||||
$createNewCommentWidgetCallback(commentControllerHandle: number, uriComponents: UriComponents, range: IRange, token: CancellationToken): void {
|
||||
const commentController = this._commentControllers.get(commentControllerHandle);
|
||||
|
||||
if (!commentControl) {
|
||||
if (!commentController || !commentController.commentingRangeProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentControl.setActiveCommentingRange(extHostTypeConverter.Range.to(range));
|
||||
const document = this._documents.getDocument(URI.revive(uriComponents));
|
||||
commentController.commentingRangeProvider.callback(document, extHostTypeConverter.Range.to(range));
|
||||
}
|
||||
|
||||
registerWorkspaceCommentProvider(
|
||||
@@ -295,10 +299,28 @@ export class ExtHostCommentThread implements vscode.CommentThread {
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
set range(range: vscode.Range) {
|
||||
this._range = range;
|
||||
this._proxy.$updateCommentThreadRange(this._commentControlHandle, this.handle, extHostTypeConverter.Range.from(this._range));
|
||||
}
|
||||
|
||||
get range(): vscode.Range {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
private _label: string;
|
||||
|
||||
get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set label(label: string) {
|
||||
this._label = label;
|
||||
this._proxy.$updateCommentThreadLabel(this._commentControlHandle, this.handle, this._label);
|
||||
}
|
||||
|
||||
private _comments: vscode.Comment[] = [];
|
||||
|
||||
get comments(): vscode.Comment[] {
|
||||
return this._comments;
|
||||
}
|
||||
@@ -308,28 +330,48 @@ export class ExtHostCommentThread implements vscode.CommentThread {
|
||||
this._comments = newComments;
|
||||
}
|
||||
|
||||
get acceptInputCommands(): vscode.Command[] {
|
||||
return this._acceptInputCommands;
|
||||
private _acceptInputCommand: vscode.Command;
|
||||
get acceptInputCommand(): vscode.Command {
|
||||
return this._acceptInputCommand;
|
||||
}
|
||||
|
||||
set acceptInputCommands(replyCommands: vscode.Command[]) {
|
||||
this._acceptInputCommands = replyCommands;
|
||||
set acceptInputCommand(acceptInputCommand: vscode.Command) {
|
||||
this._acceptInputCommand = acceptInputCommand;
|
||||
|
||||
const internals = replyCommands.map(this._commandsConverter.toInternal.bind(this._commandsConverter));
|
||||
this._proxy.$updateCommentThreadCommands(this._commentControlHandle, this.handle, internals);
|
||||
const internal = this._commandsConverter.toInternal(acceptInputCommand);
|
||||
this._proxy.$updateCommentThreadAcceptInputCommand(this._commentControlHandle, this.handle, internal);
|
||||
}
|
||||
|
||||
private _additionalCommands: vscode.Command[] = [];
|
||||
get additionalCommands(): vscode.Command[] {
|
||||
return this._additionalCommands;
|
||||
}
|
||||
|
||||
set additionalCommands(additionalCommands: vscode.Command[]) {
|
||||
this._additionalCommands = additionalCommands;
|
||||
|
||||
const internals = additionalCommands.map(this._commandsConverter.toInternal.bind(this._commandsConverter));
|
||||
this._proxy.$updateCommentThreadAdditionalCommands(this._commentControlHandle, this.handle, internals);
|
||||
}
|
||||
|
||||
private _collapseState?: vscode.CommentThreadCollapsibleState;
|
||||
|
||||
get collapsibleState(): vscode.CommentThreadCollapsibleState {
|
||||
return this._collapseState;
|
||||
}
|
||||
|
||||
set collapsibleState(newState: vscode.CommentThreadCollapsibleState) {
|
||||
this._collapseState = newState;
|
||||
this._proxy.$updateCommentThreadCollapsibleState(this._commentControlHandle, this.handle, convertToCollapsibleState(newState));
|
||||
}
|
||||
|
||||
collapsibleState?: vscode.CommentThreadCollapsibleState;
|
||||
constructor(
|
||||
private _proxy: MainThreadCommentsShape,
|
||||
private readonly _commandsConverter: CommandsConverter,
|
||||
private _commentControlHandle: number,
|
||||
private _threadId: string,
|
||||
private _resource: vscode.Uri,
|
||||
private _range: vscode.Range,
|
||||
private _comments: vscode.Comment[],
|
||||
private _acceptInputCommands: vscode.Command[],
|
||||
private _collapseState?: vscode.CommentThreadCollapsibleState
|
||||
private _range: vscode.Range
|
||||
) {
|
||||
this._proxy.$createCommentThread(
|
||||
this._commentControlHandle,
|
||||
@@ -338,7 +380,8 @@ export class ExtHostCommentThread implements vscode.CommentThread {
|
||||
this._resource,
|
||||
extHostTypeConverter.Range.from(this._range),
|
||||
this._comments.map(comment => { return convertToModeComment(comment, this._commandsConverter); }),
|
||||
this._acceptInputCommands ? this._acceptInputCommands.map(this._commandsConverter.toInternal.bind(this._commandsConverter)) : [],
|
||||
this._acceptInputCommand ? this._commandsConverter.toInternal(this._acceptInputCommand) : undefined,
|
||||
this._additionalCommands ? this._additionalCommands.map(this._commandsConverter.toInternal.bind(this._commandsConverter)) : [],
|
||||
this._collapseState
|
||||
);
|
||||
}
|
||||
@@ -361,89 +404,48 @@ export class ExtHostCommentThread implements vscode.CommentThread {
|
||||
}
|
||||
|
||||
}
|
||||
export class ExtHostCommentWidget implements vscode.CommentWidget {
|
||||
|
||||
private _onDidChangeInput = new Emitter<string>();
|
||||
export class ExtHostCommentInputBox implements vscode.CommentInputBox {
|
||||
private _onDidChangeValue = new Emitter<string>();
|
||||
|
||||
get onDidChangeInput(): Event<string> {
|
||||
return this._onDidChangeInput.event;
|
||||
get onDidChangeValue(): Event<string> {
|
||||
return this._onDidChangeValue.event;
|
||||
}
|
||||
private _value: string = '';
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
private _input: string = '';
|
||||
get input(): string {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
set input(newInput: string) {
|
||||
this._input = newInput;
|
||||
this._onDidChangeInput.fire(this._input);
|
||||
this._proxy.$setInputValue(this.commentControlHandle, this.commentThread.handle, newInput);
|
||||
set value(newInput: string) {
|
||||
this._value = newInput;
|
||||
this._onDidChangeValue.fire(this._value);
|
||||
this._proxy.$setInputValue(this.commentControllerHandle, newInput);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _proxy: MainThreadCommentsShape,
|
||||
|
||||
public commentControlHandle: number,
|
||||
|
||||
public commentThread: ExtHostCommentThread,
|
||||
public comment: vscode.Comment | undefined,
|
||||
public commentControllerHandle: number,
|
||||
input: string
|
||||
) {
|
||||
this._input = input;
|
||||
this._value = input;
|
||||
}
|
||||
|
||||
setInput(input: string) {
|
||||
this._value = input;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostCommentingRanges implements vscode.CommentingRanges {
|
||||
private static _handlePool: number = 0;
|
||||
readonly handle = ExtHostCommentingRanges._handlePool++;
|
||||
|
||||
get resource(): vscode.Uri {
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
get ranges(): vscode.Range[] {
|
||||
return this._ranges;
|
||||
}
|
||||
|
||||
set ranges(newRanges: vscode.Range[]) {
|
||||
this._ranges = newRanges;
|
||||
this._proxy.$updateCommentingRanges(this._commentControlHandle, this.handle, this._ranges.map(extHostTypeConverter.Range.from));
|
||||
}
|
||||
|
||||
get newCommentThreadCommand(): vscode.Command {
|
||||
return this._command;
|
||||
}
|
||||
|
||||
set newCommentThreadCommand(command: vscode.Command) {
|
||||
this._command = command;
|
||||
|
||||
const internal = this._commandsConverter.toInternal(command);
|
||||
this._proxy.$updateCommentingRangesCommands(this._commentControlHandle, this.handle, internal);
|
||||
}
|
||||
|
||||
class ExtHostCommentingRangeProvider {
|
||||
constructor(
|
||||
private _proxy: MainThreadCommentsShape,
|
||||
private readonly _commandsConverter: CommandsConverter,
|
||||
private _commentControlHandle: number,
|
||||
private _resource: vscode.Uri,
|
||||
private _ranges: vscode.Range[],
|
||||
private _command: vscode.Command,
|
||||
public provider: (document: vscode.TextDocument, token: vscode.CancellationToken) => vscode.ProviderResult<vscode.Range[]>,
|
||||
public callback: (document: vscode.TextDocument, range: vscode.Range) => void
|
||||
) {
|
||||
this._proxy.$createCommentingRanges(
|
||||
this._commentControlHandle,
|
||||
this.handle,
|
||||
this._resource,
|
||||
this._ranges.map(extHostTypeConverter.Range.from),
|
||||
this._commandsConverter.toInternal(this._command)
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._proxy.$deleteCommentingRanges(this._commentControlHandle, this.handle);
|
||||
}
|
||||
}
|
||||
|
||||
class ExtHostCommentControl implements vscode.CommentControl {
|
||||
class ExtHostCommentController implements vscode.CommentController {
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
@@ -452,7 +454,7 @@ class ExtHostCommentControl implements vscode.CommentControl {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
public widget?: ExtHostCommentWidget;
|
||||
public inputBox?: ExtHostCommentInputBox;
|
||||
public activeCommentingRange?: vscode.Range;
|
||||
|
||||
public get handle(): number {
|
||||
@@ -460,7 +462,16 @@ class ExtHostCommentControl implements vscode.CommentControl {
|
||||
}
|
||||
|
||||
private _threads: Map<number, ExtHostCommentThread> = new Map<number, ExtHostCommentThread>();
|
||||
private _commentingRanges: Map<number, ExtHostCommentingRanges> = new Map<number, ExtHostCommentingRanges>();
|
||||
|
||||
private _commentingRangeProvider: ExtHostCommentingRangeProvider | undefined = undefined;
|
||||
|
||||
get commentingRangeProvider(): ExtHostCommentingRangeProvider | undefined {
|
||||
return this._commentingRangeProvider;
|
||||
}
|
||||
|
||||
set commentingRangeProvider(commentingRangeProvider: ExtHostCommentingRangeProvider | undefined) {
|
||||
this._commentingRangeProvider = commentingRangeProvider;
|
||||
}
|
||||
|
||||
constructor(
|
||||
_extension: IExtensionDescription,
|
||||
@@ -470,37 +481,25 @@ class ExtHostCommentControl implements vscode.CommentControl {
|
||||
private _id: string,
|
||||
private _label: string
|
||||
) {
|
||||
this._proxy.$registerCommentControl(this.handle, _id, _label);
|
||||
this._proxy.$registerCommentController(this.handle, _id, _label);
|
||||
}
|
||||
|
||||
createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range, comments: vscode.Comment[], acceptInputCommands: vscode.Command[], collapsibleState?: vscode.CommentThreadCollapsibleState): vscode.CommentThread {
|
||||
const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this.handle, id, resource, range, comments, acceptInputCommands, collapsibleState);
|
||||
createCommentThread(id: string, resource: vscode.Uri, range: vscode.Range): vscode.CommentThread {
|
||||
const commentThread = new ExtHostCommentThread(this._proxy, this._commandsConverter, this.handle, id, resource, range);
|
||||
this._threads.set(commentThread.handle, commentThread);
|
||||
return commentThread;
|
||||
}
|
||||
|
||||
$onActiveCommentWidgetChange(commentThread: modes.CommentThread2, comment: modes.Comment | undefined, input: string) {
|
||||
const extHostCommentThread = this._threads.get(commentThread.commentThreadHandle);
|
||||
|
||||
const extHostCommentWidget = new ExtHostCommentWidget(
|
||||
this._proxy,
|
||||
this.handle,
|
||||
extHostCommentThread,
|
||||
comment ? extHostCommentThread.getComment(comment.commentId) : undefined,
|
||||
input || ''
|
||||
);
|
||||
|
||||
this.widget = extHostCommentWidget;
|
||||
registerCommentingRangeProvider(provider: (document: vscode.TextDocument, token: vscode.CancellationToken) => vscode.ProviderResult<vscode.Range[]>, callback: (document: vscode.TextDocument, range: vscode.Range) => void) {
|
||||
this._commentingRangeProvider = new ExtHostCommentingRangeProvider(provider, callback);
|
||||
}
|
||||
|
||||
createCommentingRanges(resource: vscode.Uri, ranges: vscode.Range[], newCommentThreadCommand: vscode.Command): vscode.CommentingRanges {
|
||||
const commentingRange = new ExtHostCommentingRanges(this._proxy, this._commandsConverter, this.handle, resource, ranges, newCommentThreadCommand);
|
||||
this._commentingRanges.set(commentingRange.handle, commentingRange);
|
||||
return commentingRange;
|
||||
}
|
||||
|
||||
setActiveCommentingRange(range: vscode.Range) {
|
||||
this.activeCommentingRange = range;
|
||||
$onCommentWidgetInputChange(input: string) {
|
||||
if (!this.inputBox) {
|
||||
this.inputBox = new ExtHostCommentInputBox(this._proxy, this.handle, input);
|
||||
} else {
|
||||
this.inputBox.setInput(input);
|
||||
}
|
||||
}
|
||||
|
||||
getCommentThread(handle: number) {
|
||||
@@ -511,9 +510,6 @@ class ExtHostCommentControl implements vscode.CommentControl {
|
||||
this._threads.forEach(value => {
|
||||
value.dispose();
|
||||
});
|
||||
this._commentingRanges.forEach(value => {
|
||||
value.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +581,8 @@ function convertToModeComment(vscodeComment: vscode.Comment, commandsConverter:
|
||||
userIconPath: iconPath,
|
||||
isDraft: vscodeComment.isDraft,
|
||||
editCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.editCommand) : undefined,
|
||||
deleteCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined
|
||||
deleteCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined,
|
||||
label: vscodeComment.label
|
||||
};
|
||||
}
|
||||
|
||||
@@ -626,4 +623,16 @@ function convertFromReaction(reaction: modes.CommentReaction): vscode.CommentRea
|
||||
count: reaction.count,
|
||||
hasReacted: reaction.hasReacted
|
||||
};
|
||||
}
|
||||
|
||||
function convertToCollapsibleState(kind: vscode.CommentThreadCollapsibleState | undefined): modes.CommentThreadCollapsibleState {
|
||||
if (kind !== undefined) {
|
||||
switch (kind) {
|
||||
case types.CommentThreadCollapsibleState.Expanded:
|
||||
return modes.CommentThreadCollapsibleState.Expanded;
|
||||
case types.CommentThreadCollapsibleState.Collapsed:
|
||||
return modes.CommentThreadCollapsibleState.Collapsed;
|
||||
}
|
||||
}
|
||||
return modes.CommentThreadCollapsibleState.Collapsed;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { MainContext, MainThreadOutputServiceShape, IMainContext, ExtHostOutputS
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { OutputAppender } from 'vs/workbench/contrib/output/node/outputAppender';
|
||||
import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
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 { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer';
|
||||
import { ICachedSearchStats, IFileIndexProviderStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, ISearchCompleteStats } from 'vs/workbench/services/search/common/search';
|
||||
import { IDirectoryEntry, IDirectoryTree, IInternalFileMatch } from 'vs/workbench/services/search/node/fileSearchManager';
|
||||
import { QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/node/search';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
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 | null;
|
||||
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: IFileQuery, 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): Promise<{ isLimitHit: boolean, stats: IFileIndexProviderStats }> {
|
||||
// Searches a single folder
|
||||
const folderQuery = this.config.folderQueries[0];
|
||||
|
||||
return new Promise<{ 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 Promise.all(this.config.folderQueries.map(fq => this.searchInFolder(folderQuery, onResult))).then(stats => {
|
||||
resolve({
|
||||
isLimitHit: this.isLimitHit,
|
||||
stats: {
|
||||
directoriesWalked: this.dirsWalked,
|
||||
filesWalked: this.filesWalked,
|
||||
fileWalkTime: stats.map(s => s.fileWalkTime).reduce((s, c) => s + c, 0),
|
||||
providerTime: stats.map(s => s.providerTime).reduce((s, c) => s + c, 0),
|
||||
providerResultCount: stats.map(s => s.providerResultCount).reduce((s, c) => s + c, 0)
|
||||
}
|
||||
});
|
||||
}, (errs: Error[]) => {
|
||||
if (!Array.isArray(errs)) {
|
||||
errs = [errs];
|
||||
}
|
||||
|
||||
errs = arrays.coalesce(errs);
|
||||
return Promise.reject(errs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private searchInFolder(fq: IFolderQuery<URI>, onResult: (match: IInternalFileMatch) => void): Promise<IFileIndexProviderStats> {
|
||||
const cancellation = new CancellationTokenSource();
|
||||
return new Promise((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 Promise(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: !fq.disregardIgnoreFiles,
|
||||
useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles,
|
||||
followSymlinks: !fq.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: IFileQuery, provider: vscode.FileIndexProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
|
||||
if (config.sortByScore) {
|
||||
let sortedSearch = this.trySortedSearchFromCache(config, token);
|
||||
if (!sortedSearch) {
|
||||
const engineConfig = config.maxResults ?
|
||||
{
|
||||
...config,
|
||||
...{ maxResults: null }
|
||||
} :
|
||||
config;
|
||||
|
||||
const engine = new FileIndexSearchEngine(<any>engineConfig, provider);
|
||||
sortedSearch = this.doSortedSearch(engine, config, token);
|
||||
}
|
||||
|
||||
return sortedSearch.then(complete => {
|
||||
this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE);
|
||||
return complete;
|
||||
});
|
||||
}
|
||||
|
||||
const engine = new FileIndexSearchEngine(config, provider);
|
||||
return this.doSearch(engine, token)
|
||||
.then(complete => {
|
||||
this.sendAsBatches(complete.results, onBatch, FileIndexSearchManager.BATCH_SIZE);
|
||||
return <ISearchCompleteStats>{
|
||||
limitHit: complete.limitHit,
|
||||
stats: {
|
||||
type: 'fileIndexProvider',
|
||||
detailStats: complete.stats,
|
||||
fromCache: false,
|
||||
resultCount: complete.results.length
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getFolderCacheKey(config: IFileQuery): 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: IFileQuery, token: CancellationToken): Promise<IInternalSearchComplete> {
|
||||
let allResultsPromise = createCancelablePromise<IInternalSearchComplete<IFileIndexProviderStats>>(token => {
|
||||
return this.doSearch(engine, token);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return Promise.resolve<IInternalSearchComplete>(
|
||||
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, token)
|
||||
.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;
|
||||
return <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: 'fileIndexProvider'
|
||||
}
|
||||
};
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private getOrCreateCache(cacheKey: string): Cache {
|
||||
const existing = this.caches[cacheKey];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return this.caches[cacheKey] = new Cache();
|
||||
}
|
||||
|
||||
private trySortedSearchFromCache(config: IFileQuery, token: CancellationToken): Promise<IInternalSearchComplete> | undefined {
|
||||
const folderCacheKey = this.getFolderCacheKey(config);
|
||||
const cache = folderCacheKey && this.caches[folderCacheKey];
|
||||
if (!cache) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cached = this.getResultsFromCache(cache, config.filePattern!, token);
|
||||
if (cached) {
|
||||
return cached.then(complete => {
|
||||
const sortSW = StopWatch.create();
|
||||
return this.sortResults(config, complete.results, cache.scorerCache, token)
|
||||
.then(sortedResults => {
|
||||
if (token && token.isCancellationRequested) {
|
||||
throw canceled();
|
||||
}
|
||||
|
||||
return <IInternalSearchComplete<IFileSearchStats>>{
|
||||
limitHit: complete.limitHit || typeof config.maxResults === 'number' && complete.results.length > config.maxResults,
|
||||
results: sortedResults,
|
||||
stats: {
|
||||
fromCache: true,
|
||||
detailStats: complete.stats,
|
||||
type: 'fileIndexProvider',
|
||||
resultCount: sortedResults.length,
|
||||
sortingTime: sortSW.elapsed()
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private sortResults(config: IFileQuery, results: IInternalFileMatch[], scorerCache: ScorerCache, token: CancellationToken): Promise<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, token);
|
||||
}
|
||||
|
||||
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, token: CancellationToken): Promise<IInternalSearchComplete<ICachedSearchStats>> | null {
|
||||
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 | undefined;
|
||||
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 Promise<IInternalSearchComplete<ICachedSearchStats>>((c, e) => {
|
||||
token.onCancellationRequested(() => e(canceled()));
|
||||
|
||||
cacheRow!.promise.then(complete => {
|
||||
if (token && token.isCancellationRequested) {
|
||||
e(canceled());
|
||||
}
|
||||
|
||||
// Pattern match on results
|
||||
const results: IInternalFileMatch[] = [];
|
||||
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
|
||||
for (let i = 0; i < complete.results.length; i++) {
|
||||
const 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);
|
||||
});
|
||||
}
|
||||
|
||||
private doSearch(engine: FileIndexSearchEngine, token: CancellationToken): Promise<IInternalSearchComplete<IFileIndexProviderStats>> {
|
||||
token.onCancellationRequested(() => engine.cancel());
|
||||
const results: IInternalFileMatch[] = [];
|
||||
const onResult = match => results.push(match);
|
||||
|
||||
return engine.search(onResult).then(result => {
|
||||
return <IInternalSearchComplete<IFileIndexProviderStats>>{
|
||||
limitHit: result.isLimitHit,
|
||||
results,
|
||||
stats: result.stats
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public clearCache(cacheKey: string): void {
|
||||
const expandedKeys = this.folderCacheKeys.get(cacheKey);
|
||||
if (!expandedKeys) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
expandedKeys.forEach(key => delete this.caches[key]);
|
||||
|
||||
this.folderCacheKeys.delete(cacheKey);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private preventCancellation<C>(promise: CancelablePromise<C>): CancelablePromise<C> {
|
||||
return new class implements CancelablePromise<C> {
|
||||
cancel() {
|
||||
// Do nothing
|
||||
}
|
||||
then(resolve, reject) {
|
||||
return promise.then(resolve, reject);
|
||||
}
|
||||
catch(reject?) {
|
||||
return this.then(undefined, reject);
|
||||
}
|
||||
finally(onFinally) {
|
||||
return promise.finally(onFinally);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ICacheRow {
|
||||
promise: CancelablePromise<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
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IFileQuery, IFolderQuery, IRawFileQuery, IRawQuery, IRawTextQuery, ISearchCompleteStats, ITextQuery } from 'vs/workbench/services/search/common/search';
|
||||
import { FileIndexSearchManager } from 'vs/workbench/api/node/extHostSearch.fileIndex';
|
||||
import { FileSearchManager } from 'vs/workbench/services/search/node/fileSearchManager';
|
||||
import { SearchService } from 'vs/workbench/services/search/node/rawSearchService';
|
||||
import { RipgrepSearchProvider } from 'vs/workbench/services/search/node/ripgrepSearchProvider';
|
||||
@@ -30,20 +29,16 @@ export class ExtHostSearch implements ExtHostSearchShape {
|
||||
private readonly _textSearchUsedSchemes = new Set<string>();
|
||||
private readonly _fileSearchProvider = new Map<number, vscode.FileSearchProvider>();
|
||||
private readonly _fileSearchUsedSchemes = new Set<string>();
|
||||
private readonly _fileIndexProvider = new Map<number, vscode.FileIndexProvider>();
|
||||
private readonly _fileIndexUsedSchemes = new Set<string>();
|
||||
private _handlePool: number = 0;
|
||||
|
||||
private _internalFileSearchHandle: number;
|
||||
private _internalFileSearchProvider: SearchService | null;
|
||||
|
||||
private _fileSearchManager: FileSearchManager;
|
||||
private _fileIndexSearchManager: FileIndexSearchManager;
|
||||
|
||||
constructor(mainContext: IMainContext, private _schemeTransformer: ISchemeTransformer | null, private _logService: ILogService, private _extfs = extfs) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadSearch);
|
||||
this._fileSearchManager = new FileSearchManager();
|
||||
this._fileIndexSearchManager = new FileIndexSearchManager();
|
||||
}
|
||||
|
||||
private _transformScheme(scheme: string): string {
|
||||
@@ -96,22 +91,6 @@ export class ExtHostSearch implements ExtHostSearchShape {
|
||||
});
|
||||
}
|
||||
|
||||
registerFileIndexProvider(scheme: string, provider: vscode.FileIndexProvider): IDisposable {
|
||||
if (this._fileIndexUsedSchemes.has(scheme)) {
|
||||
throw new Error(`a provider for the scheme '${scheme}' is already registered`);
|
||||
}
|
||||
|
||||
this._fileIndexUsedSchemes.add(scheme);
|
||||
const handle = this._handlePool++;
|
||||
this._fileIndexProvider.set(handle, provider);
|
||||
this._proxy.$registerFileIndexProvider(handle, this._transformScheme(scheme));
|
||||
return toDisposable(() => {
|
||||
this._fileIndexUsedSchemes.delete(scheme);
|
||||
this._fileSearchProvider.delete(handle);
|
||||
this._proxy.$unregisterProvider(handle); // TODO@roblou - unregisterFileIndexProvider
|
||||
});
|
||||
}
|
||||
|
||||
$provideFileSearchResults(handle: number, session: number, rawQuery: IRawFileQuery, token: CancellationToken): Promise<ISearchCompleteStats> {
|
||||
const query = reviveQuery(rawQuery);
|
||||
if (handle === this._internalFileSearchHandle) {
|
||||
@@ -123,14 +102,7 @@ export class ExtHostSearch implements ExtHostSearchShape {
|
||||
this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource));
|
||||
}, token);
|
||||
} else {
|
||||
const indexProvider = this._fileIndexProvider.get(handle);
|
||||
if (!indexProvider) {
|
||||
throw new Error('unknown provider: ' + handle);
|
||||
}
|
||||
|
||||
return this._fileIndexSearchManager.fileSearch(query, indexProvider, batch => {
|
||||
this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource));
|
||||
}, token);
|
||||
throw new Error('unknown provider: ' + handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +136,6 @@ export class ExtHostSearch implements ExtHostSearchShape {
|
||||
}
|
||||
|
||||
this._fileSearchManager.clearCache(cacheKey);
|
||||
this._fileIndexSearchManager.clearCache(cacheKey);
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
@@ -19,14 +19,19 @@ import * as types from 'vs/workbench/api/node/extHostTypes';
|
||||
import { ExtHostWorkspace, IExtHostWorkspaceProvider } from 'vs/workbench/api/node/extHostWorkspace';
|
||||
import * as vscode from 'vscode';
|
||||
import {
|
||||
TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO, ProcessExecutionOptionsDTO, ProcessExecutionDTO,
|
||||
ShellExecutionOptionsDTO, ShellExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, TaskSetDTO
|
||||
TaskDefinitionDTO, TaskExecutionDTO, TaskPresentationOptionsDTO,
|
||||
ProcessExecutionOptionsDTO, ProcessExecutionDTO,
|
||||
ShellExecutionOptionsDTO, ShellExecutionDTO,
|
||||
CustomExecutionDTO,
|
||||
TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, TaskSetDTO
|
||||
} from '../shared/tasks';
|
||||
import { ExtHostVariableResolverService } from 'vs/workbench/api/node/extHostDebugService';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors';
|
||||
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
|
||||
import { ExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/node/extHostTerminalService';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
namespace TaskDefinitionDTO {
|
||||
export function from(value: vscode.TaskDefinition): TaskDefinitionDTO {
|
||||
@@ -74,7 +79,7 @@ namespace ProcessExecutionOptionsDTO {
|
||||
}
|
||||
|
||||
namespace ProcessExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ProcessExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ProcessExecutionDTO {
|
||||
const candidate = value as ProcessExecutionDTO;
|
||||
return candidate && !!candidate.process;
|
||||
}
|
||||
@@ -115,7 +120,7 @@ namespace ShellExecutionOptionsDTO {
|
||||
}
|
||||
|
||||
namespace ShellExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO): value is ShellExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ShellExecutionDTO {
|
||||
const candidate = value as ShellExecutionDTO;
|
||||
return candidate && (!!candidate.commandLine || !!candidate.command);
|
||||
}
|
||||
@@ -148,6 +153,19 @@ namespace ShellExecutionDTO {
|
||||
}
|
||||
}
|
||||
|
||||
namespace CustomExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is CustomExecutionDTO {
|
||||
let candidate = value as CustomExecutionDTO;
|
||||
return candidate && candidate.customExecution === 'customExecution';
|
||||
}
|
||||
|
||||
export function from(value: vscode.CustomExecution): CustomExecutionDTO {
|
||||
return {
|
||||
customExecution: 'customExecution'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskHandleDTO {
|
||||
export function from(value: types.Task): TaskHandleDTO {
|
||||
let folder: UriComponents | undefined;
|
||||
@@ -181,11 +199,13 @@ namespace TaskDTO {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
let execution: ShellExecutionDTO | ProcessExecutionDTO | undefined;
|
||||
let execution: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO | undefined;
|
||||
if (value.execution instanceof types.ProcessExecution) {
|
||||
execution = ProcessExecutionDTO.from(value.execution);
|
||||
} else if (value.execution instanceof types.ShellExecution) {
|
||||
execution = ShellExecutionDTO.from(value.execution);
|
||||
} else if ((<vscode.Task2>value).execution2 && (<vscode.Task2>value).execution2 instanceof types.CustomExecution) {
|
||||
execution = CustomExecutionDTO.from(<types.CustomExecution>(<vscode.Task2>value).execution2);
|
||||
}
|
||||
const definition: TaskDefinitionDTO = TaskDefinitionDTO.from(value.definition);
|
||||
let scope: number | UriComponents;
|
||||
@@ -315,15 +335,121 @@ interface HandlerData {
|
||||
extension: IExtensionDescription;
|
||||
}
|
||||
|
||||
class CustomExecutionData implements IDisposable {
|
||||
private static waitForDimensionsTimeoutInMs: number = 5000;
|
||||
private _cancellationSource?: CancellationTokenSource;
|
||||
private readonly _onTaskExecutionComplete: Emitter<CustomExecutionData> = new Emitter<CustomExecutionData>();
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
private terminal?: vscode.Terminal;
|
||||
private terminalId?: number;
|
||||
public result: number | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly customExecution: vscode.CustomExecution,
|
||||
private readonly terminalService: ExtHostTerminalService) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
dispose(this._disposables);
|
||||
}
|
||||
|
||||
public get onTaskExecutionComplete(): Event<CustomExecutionData> {
|
||||
return this._onTaskExecutionComplete.event;
|
||||
}
|
||||
|
||||
private onDidCloseTerminal(terminal: vscode.Terminal): void {
|
||||
if (this.terminal === terminal) {
|
||||
this._cancellationSource.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private onDidOpenTerminal(terminal: vscode.Terminal): void {
|
||||
if (!(terminal instanceof ExtHostTerminal)) {
|
||||
throw new Error('How could this not be a extension host terminal?');
|
||||
}
|
||||
|
||||
if (this.terminalId && terminal._id === this.terminalId) {
|
||||
this.startCallback(this.terminalId);
|
||||
}
|
||||
}
|
||||
|
||||
public async startCallback(terminalId: number): Promise<void> {
|
||||
this.terminalId = terminalId;
|
||||
|
||||
// If we have already started the extension task callback, then
|
||||
// do not start it again.
|
||||
// It is completely valid for multiple terminals to be opened
|
||||
// before the one for our task.
|
||||
if (this._cancellationSource) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const callbackTerminals: vscode.Terminal[] = this.terminalService.terminals.filter((terminal) => terminal._id === terminalId);
|
||||
|
||||
if (!callbackTerminals || callbackTerminals.length === 0) {
|
||||
this._disposables.push(this.terminalService.onDidOpenTerminal(this.onDidOpenTerminal.bind(this)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (callbackTerminals.length !== 1) {
|
||||
throw new Error(`Expected to only have one terminal at this point`);
|
||||
}
|
||||
|
||||
this.terminal = callbackTerminals[0];
|
||||
const terminalRenderer: vscode.TerminalRenderer = await this.terminalService.resolveTerminalRenderer(terminalId);
|
||||
|
||||
// If we don't have the maximum dimensions yet, then we need to wait for them (but not indefinitely).
|
||||
// Custom executions will expect the dimensions to be set properly before they are launched.
|
||||
// BUT, due to the API contract VSCode has for terminals and dimensions, they are still responsible for
|
||||
// handling cases where they are not set.
|
||||
if (!terminalRenderer.maximumDimensions) {
|
||||
const dimensionTimeout: Promise<void> = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, CustomExecutionData.waitForDimensionsTimeoutInMs);
|
||||
});
|
||||
|
||||
let dimensionsRegistration: IDisposable | undefined;
|
||||
const dimensionsPromise: Promise<void> = new Promise((resolve) => {
|
||||
dimensionsRegistration = terminalRenderer.onDidChangeMaximumDimensions((newDimensions) => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.race([dimensionTimeout, dimensionsPromise]);
|
||||
if (dimensionsRegistration) {
|
||||
dimensionsRegistration.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
this._cancellationSource = new CancellationTokenSource();
|
||||
this._disposables.push(this._cancellationSource);
|
||||
|
||||
this._disposables.push(this.terminalService.onDidCloseTerminal(this.onDidCloseTerminal.bind(this)));
|
||||
|
||||
// Regardless of how the task completes, we are done with this custom execution task.
|
||||
this.customExecution.callback(terminalRenderer, this._cancellationSource.token).then(
|
||||
(success) => {
|
||||
this.result = success;
|
||||
this._onTaskExecutionComplete.fire(this);
|
||||
}, (rejected) => {
|
||||
this._onTaskExecutionComplete.fire(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostTask implements ExtHostTaskShape {
|
||||
|
||||
private _proxy: MainThreadTaskShape;
|
||||
private _workspaceProvider: IExtHostWorkspaceProvider;
|
||||
private _editorService: ExtHostDocumentsAndEditors;
|
||||
private _configurationService: ExtHostConfiguration;
|
||||
private _terminalService: ExtHostTerminalService;
|
||||
private _handleCounter: number;
|
||||
private _handlers: Map<number, HandlerData>;
|
||||
private _taskExecutions: Map<string, TaskExecutionImpl>;
|
||||
private _providedCustomExecutions: Map<string, CustomExecutionData>;
|
||||
private _activeCustomExecutions: Map<string, CustomExecutionData>;
|
||||
|
||||
private readonly _onDidExecuteTask: Emitter<vscode.TaskStartEvent> = new Emitter<vscode.TaskStartEvent>();
|
||||
private readonly _onDidTerminateTask: Emitter<vscode.TaskEndEvent> = new Emitter<vscode.TaskEndEvent>();
|
||||
@@ -331,14 +457,22 @@ export class ExtHostTask implements ExtHostTaskShape {
|
||||
private readonly _onDidTaskProcessStarted: Emitter<vscode.TaskProcessStartEvent> = new Emitter<vscode.TaskProcessStartEvent>();
|
||||
private readonly _onDidTaskProcessEnded: Emitter<vscode.TaskProcessEndEvent> = new Emitter<vscode.TaskProcessEndEvent>();
|
||||
|
||||
constructor(mainContext: IMainContext, workspaceService: ExtHostWorkspace, editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfiguration) {
|
||||
constructor(
|
||||
mainContext: IMainContext,
|
||||
workspaceService: ExtHostWorkspace,
|
||||
editorService: ExtHostDocumentsAndEditors,
|
||||
configurationService: ExtHostConfiguration,
|
||||
extHostTerminalService: ExtHostTerminalService) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadTask);
|
||||
this._workspaceProvider = workspaceService;
|
||||
this._editorService = editorService;
|
||||
this._configurationService = configurationService;
|
||||
this._terminalService = extHostTerminalService;
|
||||
this._handleCounter = 0;
|
||||
this._handlers = new Map<number, HandlerData>();
|
||||
this._taskExecutions = new Map<string, TaskExecutionImpl>();
|
||||
this._providedCustomExecutions = new Map<string, CustomExecutionData>();
|
||||
this._activeCustomExecutions = new Map<string, CustomExecutionData>();
|
||||
}
|
||||
|
||||
public registerTaskProvider(extension: IExtensionDescription, provider: vscode.TaskProvider): vscode.Disposable {
|
||||
@@ -402,7 +536,26 @@ export class ExtHostTask implements ExtHostTaskShape {
|
||||
return this._onDidExecuteTask.event;
|
||||
}
|
||||
|
||||
public async $onDidStartTask(execution: TaskExecutionDTO): Promise<void> {
|
||||
public async $onDidStartTask(execution: TaskExecutionDTO, terminalId: number): Promise<void> {
|
||||
// Once a terminal is spun up for the custom execution task this event will be fired.
|
||||
// At that point, we need to actually start the callback, but
|
||||
// only if it hasn't already begun.
|
||||
const extensionCallback: CustomExecutionData | undefined = this._providedCustomExecutions.get(execution.id);
|
||||
if (extensionCallback) {
|
||||
if (this._activeCustomExecutions.get(execution.id) !== undefined) {
|
||||
throw new Error('We should not be trying to start the same custom task executions twice.');
|
||||
}
|
||||
|
||||
this._activeCustomExecutions.set(execution.id, extensionCallback);
|
||||
|
||||
const taskExecutionComplete: IDisposable = extensionCallback.onTaskExecutionComplete(() => {
|
||||
this.customExecutionComplete(execution);
|
||||
taskExecutionComplete.dispose();
|
||||
});
|
||||
|
||||
extensionCallback.startCallback(terminalId);
|
||||
}
|
||||
|
||||
this._onDidExecuteTask.fire({
|
||||
execution: await this.getTaskExecution(execution)
|
||||
});
|
||||
@@ -415,6 +568,7 @@ export class ExtHostTask implements ExtHostTaskShape {
|
||||
public async $OnDidEndTask(execution: TaskExecutionDTO): Promise<void> {
|
||||
const _execution = await this.getTaskExecution(execution);
|
||||
this._taskExecutions.delete(execution.id);
|
||||
this.customExecutionComplete(execution);
|
||||
this._onDidTerminateTask.fire({
|
||||
execution: _execution
|
||||
});
|
||||
@@ -453,21 +607,57 @@ export class ExtHostTask implements ExtHostTaskShape {
|
||||
if (!handler) {
|
||||
return Promise.reject(new Error('no handler found'));
|
||||
}
|
||||
return asPromise(() => handler.provider.provideTasks(CancellationToken.None)).then(value => {
|
||||
const sanitized: vscode.Task[] = [];
|
||||
|
||||
// For custom execution tasks, we need to store the execution objects locally
|
||||
// since we obviously cannot send callback functions through the proxy.
|
||||
// So, clear out any existing ones.
|
||||
this._providedCustomExecutions.clear();
|
||||
|
||||
// Set up a list of task ID promises that we can wait on
|
||||
// before returning the provided tasks. The ensures that
|
||||
// our task IDs are calculated for any custom execution tasks.
|
||||
// Knowing this ID ahead of time is needed because when a task
|
||||
// start event is fired this is when the custom execution is called.
|
||||
// The task start event is also the first time we see the ID from the main
|
||||
// thread, which is too late for us because we need to save an map
|
||||
// from an ID to the custom execution function. (Kind of a cart before the horse problem).
|
||||
const taskIdPromises: Promise<void>[] = [];
|
||||
const fetchPromise = asPromise(() => handler.provider.provideTasks(CancellationToken.None)).then(value => {
|
||||
const taskDTOs: TaskDTO[] = [];
|
||||
for (let task of value) {
|
||||
if (task.definition && validTypes[task.definition.type] === true) {
|
||||
sanitized.push(task);
|
||||
} else {
|
||||
sanitized.push(task);
|
||||
if (!task.definition || !validTypes[task.definition.type]) {
|
||||
console.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`);
|
||||
}
|
||||
|
||||
const taskDTO: TaskDTO = TaskDTO.from(task, handler.extension);
|
||||
taskDTOs.push(taskDTO);
|
||||
|
||||
if (CustomExecutionDTO.is(taskDTO.execution)) {
|
||||
taskIdPromises.push(new Promise((resolve) => {
|
||||
// The ID is calculated on the main thread task side, so, let's call into it here.
|
||||
// We need the task id's pre-computed for custom task executions because when OnDidStartTask
|
||||
// is invoked, we have to be able to map it back to our data.
|
||||
this._proxy.$createTaskId(taskDTO).then((taskId) => {
|
||||
this._providedCustomExecutions.set(taskId, new CustomExecutionData(<vscode.CustomExecution>(<vscode.Task2>task).execution2, this._terminalService));
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: TaskDTO.fromMany(sanitized, handler.extension),
|
||||
tasks: taskDTOs,
|
||||
extension: handler.extension
|
||||
};
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
fetchPromise.then((result) => {
|
||||
Promise.all(taskIdPromises).then(() => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }> {
|
||||
@@ -525,4 +715,13 @@ export class ExtHostTask implements ExtHostTaskShape {
|
||||
this._taskExecutions.set(execution.id, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private customExecutionComplete(execution: TaskExecutionDTO): void {
|
||||
const extensionCallback: CustomExecutionData | undefined = this._activeCustomExecutions.get(execution.id);
|
||||
if (extensionCallback) {
|
||||
this._activeCustomExecutions.delete(execution.id);
|
||||
this._proxy.$customExecutionComplete(execution.id, extensionCallback.result);
|
||||
extensionCallback.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +233,17 @@ export class ExtHostTerminalRenderer extends BaseExtHostTerminal implements vsco
|
||||
constructor(
|
||||
proxy: MainThreadTerminalServiceShape,
|
||||
private _name: string,
|
||||
private _terminal: ExtHostTerminal
|
||||
private _terminal: ExtHostTerminal,
|
||||
id?: number
|
||||
) {
|
||||
super(proxy);
|
||||
this._proxy.$createTerminalRenderer(this._name).then(id => {
|
||||
this._runQueuedRequests(id);
|
||||
(<any>this._terminal)._runQueuedRequests(id);
|
||||
});
|
||||
super(proxy, id);
|
||||
|
||||
if (!id) {
|
||||
this._proxy.$createTerminalRenderer(this._name).then(id => {
|
||||
this._runQueuedRequests(id);
|
||||
(<any>this._terminal)._runQueuedRequests(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public write(data: string): void {
|
||||
@@ -313,6 +317,21 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public async resolveTerminalRenderer(id: number): Promise<vscode.TerminalRenderer> {
|
||||
// Check to see if the extension host already knows about this terminal.
|
||||
for (const terminalRenderer of this._terminalRenderers) {
|
||||
if (terminalRenderer._id === id) {
|
||||
return terminalRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
const terminal = this._getTerminalById(id);
|
||||
const renderer = new ExtHostTerminalRenderer(this._proxy, terminal.name, terminal, terminal._id);
|
||||
this._terminalRenderers.push(renderer);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public $acceptActiveTerminalChanged(id: number | null): void {
|
||||
const original = this._activeTerminal;
|
||||
if (id === null) {
|
||||
@@ -372,11 +391,10 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
|
||||
|
||||
public $acceptTerminalClosed(id: number): void {
|
||||
const index = this._getTerminalObjectIndexById(this.terminals, id);
|
||||
if (index === null) {
|
||||
return;
|
||||
if (index !== null) {
|
||||
const terminal = this._terminals.splice(index, 1)[0];
|
||||
this._onDidCloseTerminal.fire(terminal);
|
||||
}
|
||||
const terminal = this._terminals.splice(index, 1)[0];
|
||||
this._onDidCloseTerminal.fire(terminal);
|
||||
}
|
||||
|
||||
public $acceptTerminalOpened(id: number, name: string): void {
|
||||
@@ -386,6 +404,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
|
||||
this._onDidOpenTerminal.fire(this.terminals[index]);
|
||||
return;
|
||||
}
|
||||
|
||||
const renderer = this._getTerminalRendererById(id);
|
||||
const terminal = new ExtHostTerminal(this._proxy, name, id, renderer ? RENDERER_NO_PROCESS_ID : undefined);
|
||||
this._terminals.push(terminal);
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ExtHostEditors implements ExtHostEditorsShape {
|
||||
}
|
||||
|
||||
return this._proxy.$tryShowTextDocument(document.uri, options).then(id => {
|
||||
const editor = this._extHostDocumentsAndEditors.getEditor(id);
|
||||
const editor = id && this._extHostDocumentsAndEditors.getEditor(id);
|
||||
if (editor) {
|
||||
return editor;
|
||||
} else {
|
||||
|
||||
@@ -1708,9 +1708,33 @@ export enum TaskScope {
|
||||
Workspace = 2
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class Task implements vscode.Task {
|
||||
export class CustomExecution implements vscode.CustomExecution {
|
||||
private _callback: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable<number>;
|
||||
|
||||
constructor(callback: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable<number>) {
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
public computeId(): string {
|
||||
const hash = crypto.createHash('md5');
|
||||
hash.update('customExecution');
|
||||
hash.update(generateUuid());
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
public set callback(value: (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable<number>) {
|
||||
this._callback = value;
|
||||
}
|
||||
|
||||
public get callback(): (args: vscode.TerminalRenderer, cancellationToken: vscode.CancellationToken) => Thenable<number> {
|
||||
return this._callback;
|
||||
}
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class Task implements vscode.Task2 {
|
||||
|
||||
private static ExtensionCallbackType: string = 'customExecution';
|
||||
private static ProcessType: string = 'process';
|
||||
private static ShellType: string = 'shell';
|
||||
private static EmptyType: string = '$empty';
|
||||
@@ -1720,7 +1744,7 @@ export class Task implements vscode.Task {
|
||||
private _definition: vscode.TaskDefinition;
|
||||
private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined;
|
||||
private _name: string;
|
||||
private _execution: ProcessExecution | ShellExecution | undefined;
|
||||
private _execution: ProcessExecution | ShellExecution | CustomExecution | undefined;
|
||||
private _problemMatchers: string[];
|
||||
private _hasDefinedMatchers: boolean;
|
||||
private _isBackground: boolean;
|
||||
@@ -1729,8 +1753,8 @@ export class Task implements vscode.Task {
|
||||
private _presentationOptions: vscode.TaskPresentationOptions;
|
||||
private _runOptions: vscode.RunOptions;
|
||||
|
||||
constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]);
|
||||
constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]);
|
||||
constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]);
|
||||
constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]);
|
||||
constructor(definition: vscode.TaskDefinition, arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, arg3: any, arg4?: any, arg5?: any, arg6?: any) {
|
||||
this.definition = definition;
|
||||
let problemMatchers: string | string[];
|
||||
@@ -1795,6 +1819,11 @@ export class Task implements vscode.Task {
|
||||
type: Task.ShellType,
|
||||
id: this._execution.computeId()
|
||||
};
|
||||
} else if (this._execution instanceof CustomExecution) {
|
||||
this._definition = {
|
||||
type: Task.ExtensionCallbackType,
|
||||
id: this._execution.computeId()
|
||||
};
|
||||
} else {
|
||||
this._definition = {
|
||||
type: Task.EmptyType,
|
||||
@@ -1837,17 +1866,25 @@ export class Task implements vscode.Task {
|
||||
}
|
||||
|
||||
get execution(): ProcessExecution | ShellExecution | undefined {
|
||||
return this._execution;
|
||||
return (this._execution instanceof CustomExecution) ? undefined : this._execution;
|
||||
}
|
||||
|
||||
set execution(value: ProcessExecution | ShellExecution | undefined) {
|
||||
this.execution2 = value;
|
||||
}
|
||||
|
||||
get execution2(): ProcessExecution | ShellExecution | CustomExecution | undefined {
|
||||
return this._execution;
|
||||
}
|
||||
|
||||
set execution2(value: ProcessExecution | ShellExecution | CustomExecution | undefined) {
|
||||
if (value === null) {
|
||||
value = undefined;
|
||||
}
|
||||
this.clear();
|
||||
this._execution = value;
|
||||
const type = this._definition.type;
|
||||
if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) {
|
||||
if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type || Task.ExtensionCallbackType === type) {
|
||||
this.computeDefinitionBasedOnExecution();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,10 +321,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
|
||||
return folders[0].uri.fsPath;
|
||||
}
|
||||
|
||||
getRelativePath(pathOrUri: string | vscode.Uri, includeWorkspace?: boolean): string | undefined {
|
||||
getRelativePath(pathOrUri: string | vscode.Uri, includeWorkspace?: boolean): string {
|
||||
|
||||
let resource: URI | undefined;
|
||||
let path: string | undefined;
|
||||
let path: string = '';
|
||||
if (typeof pathOrUri === 'string') {
|
||||
resource = URI.file(pathOrUri);
|
||||
path = pathOrUri;
|
||||
@@ -354,7 +354,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
|
||||
if (includeWorkspace && folder.name) {
|
||||
result = `${folder.name}/${result}`;
|
||||
}
|
||||
return result;
|
||||
return result!;
|
||||
}
|
||||
|
||||
private trySetWorkspaceFolders(folders: vscode.WorkspaceFolder[]): void {
|
||||
|
||||
Reference in New Issue
Block a user