Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Allow text search providers to give messages with links that trigger researching. #123213

Merged
merged 6 commits into from May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/vs/platform/opener/browser/link.ts
Expand Up @@ -18,6 +18,10 @@ export interface ILinkDescriptor {
readonly title?: string;
}

export interface ILinkOptions {
readonly opener?: (href: string) => void;
}

export interface ILinkStyles {
readonly textLinkForeground?: Color;
readonly disabled?: boolean;
Expand All @@ -33,6 +37,7 @@ export class Link extends Disposable {

constructor(
link: ILinkDescriptor,
options: ILinkOptions | undefined = undefined,
@IOpenerService openerService: IOpenerService
) {
super();
Expand All @@ -53,7 +58,11 @@ export class Link extends Disposable {
this._register(onOpen(e => {
EventHelper.stop(e, true);
if (!this.disabled) {
openerService.open(link.href, { allowCommands: true });
if (options?.opener) {
options.opener(link.href);
} else {
openerService.open(link.href, { allowCommands: true });
}
}
}));

Expand Down
23 changes: 22 additions & 1 deletion src/vs/vscode.proposed.d.ts
Expand Up @@ -392,6 +392,25 @@ declare module 'vscode' {
Warning = 2,
}

/**
* A message regarding a completed search.
*/
export interface TextSearchCompleteMessage {
/**
* Markdown text of the message.
*/
text: string,
/**
* Whether the source of the message is trusted, command links are disabled for untrusted message sources.
* Messaged are untrusted by default.
*/
trusted?: boolean,
/**
* The message type, this affects how the message will be rendered.
*/
type: TextSearchCompleteMessageType,
}

/**
* Information collected when text search is complete.
*/
Expand All @@ -411,8 +430,10 @@ declare module 'vscode' {
* Messages with "Information" tyle support links in markdown syntax:
* - Click to [run a command](command:workbench.action.OpenQuickPick)
* - Click to [open a website](https://aka.ms)
*
* Commands may optionally return { triggerSearch: true } to signal to VS Code that the original search should run be agian.
*/
message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[];
message?: TextSearchCompleteMessage | TextSearchCompleteMessage[];
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/parts/views/viewPane.ts
Expand Up @@ -579,7 +579,7 @@ export abstract class ViewPane extends Pane implements IView {
if (typeof node === 'string') {
append(p, document.createTextNode(node));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last parameter is optional, does the instantiation service make you pass it in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TS doesn't allow the required service injection parameters follow an optional parameter.

append(p, link.el);
disposables.add(link);
disposables.add(attachLinkStyler(link, this.themeService));
Expand Down
31 changes: 27 additions & 4 deletions src/vs/workbench/contrib/search/browser/searchView.ts
Expand Up @@ -19,6 +19,7 @@ import { Iterable } from 'vs/base/common/iterator';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { parseLinkedText } from 'vs/base/common/linkedText';
import { Schemas } from 'vs/base/common/network';
import * as env from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
Expand All @@ -35,6 +36,7 @@ import * as nls from 'vs/nls';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
Expand Down Expand Up @@ -75,6 +77,7 @@ import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/search';
import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';

const $ = dom.$;
Expand Down Expand Up @@ -159,6 +162,7 @@ export class SearchView extends ViewPane {
@IProgressService private readonly progressService: IProgressService,
@INotificationService private readonly notificationService: INotificationService,
@IDialogService private readonly dialogService: IDialogService,
@ICommandService private readonly commandService: ICommandService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
Expand Down Expand Up @@ -1524,7 +1528,7 @@ export class SearchView extends ViewPane {
if (completed && completed.messages) {
for (const message of completed.messages) {
if (message.type === TextSearchCompleteMessageType.Information) {
this.addMessage(message.text);
this.addMessage(message);
}
else if (message.type === TextSearchCompleteMessageType.Warning) {
warningMessage += (warningMessage ? ' - ' : '') + message.text;
Expand Down Expand Up @@ -1641,8 +1645,8 @@ export class SearchView extends ViewPane {
}
}

private addMessage(message: string) {
const linkedText = parseLinkedText(message);
private addMessage(message: TextSearchCompleteMessage) {
const linkedText = parseLinkedText(message.text);

const messageBox = this.messagesElement.firstChild as HTMLDivElement;
if (!messageBox) {
Expand All @@ -1659,7 +1663,26 @@ export class SearchView extends ViewPane {
if (typeof node === 'string') {
dom.append(span, document.createTextNode(node));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {
opener: async href => {
if (!message.trusted) { return; }
const parsed = URI.parse(href, true);
if (parsed.scheme === Schemas.command && message.trusted) {
const result = await this.commandService.executeCommand(parsed.path);
JacksonKearl marked this conversation as resolved.
Show resolved Hide resolved
if ((result as any)?.triggerSearch) {
this.triggerQueryChange();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What command are you going to use this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remoteHub.enableIndexing

}
} else if (parsed.scheme === Schemas.https) {
this.openerService.open(parsed);
} else {
if (parsed.scheme === Schemas.command && !message.trusted) {
this.notificationService.error(nls.localize('unable to open trust', "Unable to open command link from untrusted source: {0}", href));
} else {
this.notificationService.error(nls.localize('unable to open', "Unable to open unknown link: {0}", href));
}
}
}
});
dom.append(span, link.el);
this.messageDisposables.add(link);
this.messageDisposables.add(attachLinkStyler(link, this.themeService));
Expand Down
36 changes: 31 additions & 5 deletions src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts
Expand Up @@ -55,6 +55,10 @@ import { IFileService } from 'vs/platform/files/common/files';
import { parseLinkedText } from 'vs/base/common/linkedText';
import { Link } from 'vs/platform/opener/browser/link';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { Schemas } from 'vs/base/common/network';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes';
import { INotificationService } from 'vs/platform/notification/common/notification';

const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
const FILE_LINE_REGEX = /^(\S.*):$/;
Expand Down Expand Up @@ -97,6 +101,8 @@ export class SearchEditor extends BaseTextEditor {
@IContextViewService private readonly contextViewService: IContextViewService,
@ICommandService private readonly commandService: ICommandService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IOpenerService readonly openerService: IOpenerService,
@INotificationService private readonly notificationService: INotificationService,
@IEditorProgressService readonly progressService: IEditorProgressService,
@ITextResourceConfigurationService textResourceService: ITextResourceConfigurationService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
Expand Down Expand Up @@ -557,7 +563,7 @@ export class SearchEditor extends BaseTextEditor {
if (searchOperation && searchOperation.messages) {
for (const message of searchOperation.messages) {
if (message.type === TextSearchCompleteMessageType.Information) {
this.addMessage(message.text);
this.addMessage(message);
}
else if (message.type === TextSearchCompleteMessageType.Warning) {
warningMessage += (warningMessage ? ' - ' : '') + message.text;
Expand All @@ -576,8 +582,8 @@ export class SearchEditor extends BaseTextEditor {
input.setMatchRanges(results.matchRanges);
}

private addMessage(message: string) {
const linkedText = parseLinkedText(message);
private addMessage(message: TextSearchCompleteMessage) {
const linkedText = parseLinkedText(message.text);

let messageBox: HTMLElement;
if (this.messageBox.firstChild) {
Expand All @@ -594,7 +600,25 @@ export class SearchEditor extends BaseTextEditor {
if (typeof node === 'string') {
DOM.append(messageBox, document.createTextNode(node));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {
opener: async href => {
const parsed = URI.parse(href, true);
if (parsed.scheme === Schemas.command && message.trusted) {
const result = await this.commandService.executeCommand(parsed.path);
if ((result as any)?.triggerSearch) {
this.triggerSearch();
}
} else if (parsed.scheme === Schemas.https) {
this.openerService.open(parsed);
} else {
if (parsed.scheme === Schemas.command && !message.trusted) {
this.notificationService.error(localize('unable to open trust', "Unable to open command link from untrusted source: {0}", href));
} else {
this.notificationService.error(localize('unable to open', "Unable to open unknown link: {0}", href));
}
}
}
});
DOM.append(messageBox, link.el);
this.messageDisposables.add(link);
this.messageDisposables.add(attachLinkStyler(link, this.themeService));
Expand Down Expand Up @@ -652,7 +676,9 @@ export class SearchEditor extends BaseTextEditor {
this.saveViewState();

await super.setInput(newInput, options, context, token);
if (token.isCancellationRequested) { return; }
if (token.isCancellationRequested) {
return;
}

const { configurationModel, resultsModel } = await newInput.getModels();
if (token.isCancellationRequested) { return; }
Expand Down
Expand Up @@ -902,7 +902,7 @@ export class GettingStartedPage extends EditorPane {
if (typeof node === 'string') {
append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true }));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {});

append(p, link.el);
this.detailsPageDisposables.add(link);
Expand Down
Expand Up @@ -195,7 +195,7 @@ export class WorkspaceTrustEditor extends EditorPane {
if (typeof node === 'string') {
append(p, document.createTextNode(node));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {});
append(p, link.el);
this.rerenderDisposables.add(link);
this.rerenderDisposables.add(attachLinkStyler(link, this.themeService));
Expand Down Expand Up @@ -399,7 +399,7 @@ export class WorkspaceTrustEditor extends EditorPane {
if (typeof node === 'string') {
append(text, document.createTextNode(node));
} else {
const link = this.instantiationService.createInstance(Link, node);
const link = this.instantiationService.createInstance(Link, node, {});
append(text, link.el);
this.rerenderDisposables.add(link);
this.rerenderDisposables.add(attachLinkStyler(link, this.themeService));
Expand Down
12 changes: 9 additions & 3 deletions src/vs/workbench/services/search/common/search.ts
Expand Up @@ -206,9 +206,15 @@ export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProg
return !!(p as IProgressMessage).message;
}

export interface ITextSearchCompleteMessage {
text: string;
type: TextSearchCompleteMessageType;
trusted: boolean;
}

export interface ISearchCompleteStats {
limitHit?: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[];
messages: ITextSearchCompleteMessage[];
stats?: IFileSearchStats | ITextSearchStats;
}

Expand Down Expand Up @@ -508,13 +514,13 @@ export interface ISearchEngine<T> {
export interface ISerializedSearchSuccess {
type: 'success';
limitHit: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[];
messages: ITextSearchCompleteMessage[];
stats?: IFileSearchStats | ITextSearchStats;
}

export interface ISearchEngineSuccess {
limitHit: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[];
messages: ITextSearchCompleteMessage[];
stats: ISearchEngineStats;
}

Expand Down
20 changes: 19 additions & 1 deletion src/vs/workbench/services/search/common/searchExtTypes.ts
Expand Up @@ -224,6 +224,24 @@ export enum TextSearchCompleteMessageType {
Warning = 2,
}

/**
* A message regarding a completed search.
*/
export interface TextSearchCompleteMessage {
/**
* Markdown text of the message.
*/
text: string,
/**
* Whether the source of the message is trusted, command links are disabled for untrusted message sources.
*/
trusted: boolean,
/**
* The message type, this affects how the message will be rendered.
*/
type: TextSearchCompleteMessageType,
}

/**
* Information collected when text search is complete.
*/
Expand All @@ -244,7 +262,7 @@ export interface TextSearchComplete {
* - Click to [run a command](command:workbench.action.OpenQuickPick)
* - Click to [open a website](https://aka.ms)
*/
message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[];
message?: TextSearchCompleteMessage | TextSearchCompleteMessage[];
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/services/search/common/searchService.ts
Expand Up @@ -153,7 +153,7 @@ export class SearchService extends Disposable implements ISearchService {
return {
limitHit: completes[0] && completes[0].limitHit,
stats: completes[0].stats,
messages: arrays.coalesce(arrays.flatten(completes.map(i => i.messages))).filter(arrays.uniqueFilter(message => message.type + message.text)),
messages: arrays.coalesce(arrays.flatten(completes.map(i => i.messages))).filter(arrays.uniqueFilter(message => message.type + message.text + message.trusted)),
results: arrays.flatten(completes.map((c: ISearchComplete) => c.results))
};
})();
Expand Down