未验证 提交 ea39e2b6 编写于 作者: J Jackson Kearl 提交者: GitHub

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

上级 0988e056
...@@ -18,6 +18,10 @@ export interface ILinkDescriptor { ...@@ -18,6 +18,10 @@ export interface ILinkDescriptor {
readonly title?: string; readonly title?: string;
} }
export interface ILinkOptions {
readonly opener?: (href: string) => void;
}
export interface ILinkStyles { export interface ILinkStyles {
readonly textLinkForeground?: Color; readonly textLinkForeground?: Color;
readonly disabled?: boolean; readonly disabled?: boolean;
...@@ -33,6 +37,7 @@ export class Link extends Disposable { ...@@ -33,6 +37,7 @@ export class Link extends Disposable {
constructor( constructor(
link: ILinkDescriptor, link: ILinkDescriptor,
options: ILinkOptions | undefined = undefined,
@IOpenerService openerService: IOpenerService @IOpenerService openerService: IOpenerService
) { ) {
super(); super();
...@@ -53,7 +58,11 @@ export class Link extends Disposable { ...@@ -53,7 +58,11 @@ export class Link extends Disposable {
this._register(onOpen(e => { this._register(onOpen(e => {
EventHelper.stop(e, true); EventHelper.stop(e, true);
if (!this.disabled) { if (!this.disabled) {
openerService.open(link.href, { allowCommands: true }); if (options?.opener) {
options.opener(link.href);
} else {
openerService.open(link.href, { allowCommands: true });
}
} }
})); }));
......
...@@ -392,6 +392,25 @@ declare module 'vscode' { ...@@ -392,6 +392,25 @@ declare module 'vscode' {
Warning = 2, 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. * Information collected when text search is complete.
*/ */
...@@ -411,8 +430,10 @@ declare module 'vscode' { ...@@ -411,8 +430,10 @@ declare module 'vscode' {
* Messages with "Information" tyle support links in markdown syntax: * Messages with "Information" tyle support links in markdown syntax:
* - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [run a command](command:workbench.action.OpenQuickPick)
* - Click to [open a website](https://aka.ms) * - 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[];
} }
/** /**
......
...@@ -579,7 +579,7 @@ export abstract class ViewPane extends Pane implements IView { ...@@ -579,7 +579,7 @@ export abstract class ViewPane extends Pane implements IView {
if (typeof node === 'string') { if (typeof node === 'string') {
append(p, document.createTextNode(node)); append(p, document.createTextNode(node));
} else { } else {
const link = this.instantiationService.createInstance(Link, node); const link = this.instantiationService.createInstance(Link, node, {});
append(p, link.el); append(p, link.el);
disposables.add(link); disposables.add(link);
disposables.add(attachLinkStyler(link, this.themeService)); disposables.add(attachLinkStyler(link, this.themeService));
......
...@@ -19,6 +19,7 @@ import { Iterable } from 'vs/base/common/iterator'; ...@@ -19,6 +19,7 @@ import { Iterable } from 'vs/base/common/iterator';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { parseLinkedText } from 'vs/base/common/linkedText'; import { parseLinkedText } from 'vs/base/common/linkedText';
import { Schemas } from 'vs/base/common/network';
import * as env from 'vs/base/common/platform'; import * as env from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings'; import * as strings from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
...@@ -35,6 +36,7 @@ import * as nls from 'vs/nls'; ...@@ -35,6 +36,7 @@ import * as nls from 'vs/nls';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
...@@ -75,6 +77,7 @@ import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/ ...@@ -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 { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; 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 { 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'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
const $ = dom.$; const $ = dom.$;
...@@ -159,6 +162,7 @@ export class SearchView extends ViewPane { ...@@ -159,6 +162,7 @@ export class SearchView extends ViewPane {
@IProgressService private readonly progressService: IProgressService, @IProgressService private readonly progressService: IProgressService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@IDialogService private readonly dialogService: IDialogService, @IDialogService private readonly dialogService: IDialogService,
@ICommandService private readonly commandService: ICommandService,
@IContextViewService private readonly contextViewService: IContextViewService, @IContextViewService private readonly contextViewService: IContextViewService,
@IInstantiationService instantiationService: IInstantiationService, @IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService,
...@@ -1524,7 +1528,7 @@ export class SearchView extends ViewPane { ...@@ -1524,7 +1528,7 @@ export class SearchView extends ViewPane {
if (completed && completed.messages) { if (completed && completed.messages) {
for (const message of completed.messages) { for (const message of completed.messages) {
if (message.type === TextSearchCompleteMessageType.Information) { if (message.type === TextSearchCompleteMessageType.Information) {
this.addMessage(message.text); this.addMessage(message);
} }
else if (message.type === TextSearchCompleteMessageType.Warning) { else if (message.type === TextSearchCompleteMessageType.Warning) {
warningMessage += (warningMessage ? ' - ' : '') + message.text; warningMessage += (warningMessage ? ' - ' : '') + message.text;
...@@ -1641,8 +1645,8 @@ export class SearchView extends ViewPane { ...@@ -1641,8 +1645,8 @@ export class SearchView extends ViewPane {
} }
} }
private addMessage(message: string) { private addMessage(message: TextSearchCompleteMessage) {
const linkedText = parseLinkedText(message); const linkedText = parseLinkedText(message.text);
const messageBox = this.messagesElement.firstChild as HTMLDivElement; const messageBox = this.messagesElement.firstChild as HTMLDivElement;
if (!messageBox) { if (!messageBox) {
...@@ -1659,7 +1663,26 @@ export class SearchView extends ViewPane { ...@@ -1659,7 +1663,26 @@ export class SearchView extends ViewPane {
if (typeof node === 'string') { if (typeof node === 'string') {
dom.append(span, document.createTextNode(node)); dom.append(span, document.createTextNode(node));
} else { } 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);
if ((result as any)?.triggerSearch) {
this.triggerQueryChange();
}
} 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); dom.append(span, link.el);
this.messageDisposables.add(link); this.messageDisposables.add(link);
this.messageDisposables.add(attachLinkStyler(link, this.themeService)); this.messageDisposables.add(attachLinkStyler(link, this.themeService));
......
...@@ -55,6 +55,10 @@ import { IFileService } from 'vs/platform/files/common/files'; ...@@ -55,6 +55,10 @@ import { IFileService } from 'vs/platform/files/common/files';
import { parseLinkedText } from 'vs/base/common/linkedText'; import { parseLinkedText } from 'vs/base/common/linkedText';
import { Link } from 'vs/platform/opener/browser/link'; import { Link } from 'vs/platform/opener/browser/link';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; 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 RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
const FILE_LINE_REGEX = /^(\S.*):$/; const FILE_LINE_REGEX = /^(\S.*):$/;
...@@ -97,6 +101,8 @@ export class SearchEditor extends BaseTextEditor { ...@@ -97,6 +101,8 @@ export class SearchEditor extends BaseTextEditor {
@IContextViewService private readonly contextViewService: IContextViewService, @IContextViewService private readonly contextViewService: IContextViewService,
@ICommandService private readonly commandService: ICommandService, @ICommandService private readonly commandService: ICommandService,
@IContextKeyService readonly contextKeyService: IContextKeyService, @IContextKeyService readonly contextKeyService: IContextKeyService,
@IOpenerService readonly openerService: IOpenerService,
@INotificationService private readonly notificationService: INotificationService,
@IEditorProgressService readonly progressService: IEditorProgressService, @IEditorProgressService readonly progressService: IEditorProgressService,
@ITextResourceConfigurationService textResourceService: ITextResourceConfigurationService, @ITextResourceConfigurationService textResourceService: ITextResourceConfigurationService,
@IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorGroupsService editorGroupService: IEditorGroupsService,
...@@ -557,7 +563,7 @@ export class SearchEditor extends BaseTextEditor { ...@@ -557,7 +563,7 @@ export class SearchEditor extends BaseTextEditor {
if (searchOperation && searchOperation.messages) { if (searchOperation && searchOperation.messages) {
for (const message of searchOperation.messages) { for (const message of searchOperation.messages) {
if (message.type === TextSearchCompleteMessageType.Information) { if (message.type === TextSearchCompleteMessageType.Information) {
this.addMessage(message.text); this.addMessage(message);
} }
else if (message.type === TextSearchCompleteMessageType.Warning) { else if (message.type === TextSearchCompleteMessageType.Warning) {
warningMessage += (warningMessage ? ' - ' : '') + message.text; warningMessage += (warningMessage ? ' - ' : '') + message.text;
...@@ -576,8 +582,8 @@ export class SearchEditor extends BaseTextEditor { ...@@ -576,8 +582,8 @@ export class SearchEditor extends BaseTextEditor {
input.setMatchRanges(results.matchRanges); input.setMatchRanges(results.matchRanges);
} }
private addMessage(message: string) { private addMessage(message: TextSearchCompleteMessage) {
const linkedText = parseLinkedText(message); const linkedText = parseLinkedText(message.text);
let messageBox: HTMLElement; let messageBox: HTMLElement;
if (this.messageBox.firstChild) { if (this.messageBox.firstChild) {
...@@ -594,7 +600,25 @@ export class SearchEditor extends BaseTextEditor { ...@@ -594,7 +600,25 @@ export class SearchEditor extends BaseTextEditor {
if (typeof node === 'string') { if (typeof node === 'string') {
DOM.append(messageBox, document.createTextNode(node)); DOM.append(messageBox, document.createTextNode(node));
} else { } 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); DOM.append(messageBox, link.el);
this.messageDisposables.add(link); this.messageDisposables.add(link);
this.messageDisposables.add(attachLinkStyler(link, this.themeService)); this.messageDisposables.add(attachLinkStyler(link, this.themeService));
...@@ -652,7 +676,9 @@ export class SearchEditor extends BaseTextEditor { ...@@ -652,7 +676,9 @@ export class SearchEditor extends BaseTextEditor {
this.saveViewState(); this.saveViewState();
await super.setInput(newInput, options, context, token); await super.setInput(newInput, options, context, token);
if (token.isCancellationRequested) { return; } if (token.isCancellationRequested) {
return;
}
const { configurationModel, resultsModel } = await newInput.getModels(); const { configurationModel, resultsModel } = await newInput.getModels();
if (token.isCancellationRequested) { return; } if (token.isCancellationRequested) { return; }
......
...@@ -912,7 +912,7 @@ export class GettingStartedPage extends EditorPane { ...@@ -912,7 +912,7 @@ export class GettingStartedPage extends EditorPane {
if (typeof node === 'string') { if (typeof node === 'string') {
append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true })); append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true }));
} else { } else {
const link = this.instantiationService.createInstance(Link, node); const link = this.instantiationService.createInstance(Link, node, {});
append(p, link.el); append(p, link.el);
this.detailsPageDisposables.add(link); this.detailsPageDisposables.add(link);
......
...@@ -187,7 +187,7 @@ export class WorkspaceTrustEditor extends EditorPane { ...@@ -187,7 +187,7 @@ export class WorkspaceTrustEditor extends EditorPane {
if (typeof node === 'string') { if (typeof node === 'string') {
append(p, document.createTextNode(node)); append(p, document.createTextNode(node));
} else { } else {
const link = this.instantiationService.createInstance(Link, node); const link = this.instantiationService.createInstance(Link, node, {});
append(p, link.el); append(p, link.el);
this.rerenderDisposables.add(link); this.rerenderDisposables.add(link);
this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); this.rerenderDisposables.add(attachLinkStyler(link, this.themeService));
...@@ -394,7 +394,7 @@ export class WorkspaceTrustEditor extends EditorPane { ...@@ -394,7 +394,7 @@ export class WorkspaceTrustEditor extends EditorPane {
if (typeof node === 'string') { if (typeof node === 'string') {
append(text, document.createTextNode(node)); append(text, document.createTextNode(node));
} else { } else {
const link = this.instantiationService.createInstance(Link, node); const link = this.instantiationService.createInstance(Link, node, {});
append(text, link.el); append(text, link.el);
this.rerenderDisposables.add(link); this.rerenderDisposables.add(link);
this.rerenderDisposables.add(attachLinkStyler(link, this.themeService)); this.rerenderDisposables.add(attachLinkStyler(link, this.themeService));
......
...@@ -206,9 +206,15 @@ export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProg ...@@ -206,9 +206,15 @@ export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProg
return !!(p as IProgressMessage).message; return !!(p as IProgressMessage).message;
} }
export interface ITextSearchCompleteMessage {
text: string;
type: TextSearchCompleteMessageType;
trusted: boolean;
}
export interface ISearchCompleteStats { export interface ISearchCompleteStats {
limitHit?: boolean; limitHit?: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[]; messages: ITextSearchCompleteMessage[];
stats?: IFileSearchStats | ITextSearchStats; stats?: IFileSearchStats | ITextSearchStats;
} }
...@@ -508,13 +514,13 @@ export interface ISearchEngine<T> { ...@@ -508,13 +514,13 @@ export interface ISearchEngine<T> {
export interface ISerializedSearchSuccess { export interface ISerializedSearchSuccess {
type: 'success'; type: 'success';
limitHit: boolean; limitHit: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[]; messages: ITextSearchCompleteMessage[];
stats?: IFileSearchStats | ITextSearchStats; stats?: IFileSearchStats | ITextSearchStats;
} }
export interface ISearchEngineSuccess { export interface ISearchEngineSuccess {
limitHit: boolean; limitHit: boolean;
messages: { text: string, type: TextSearchCompleteMessageType }[]; messages: ITextSearchCompleteMessage[];
stats: ISearchEngineStats; stats: ISearchEngineStats;
} }
......
...@@ -224,6 +224,24 @@ export enum TextSearchCompleteMessageType { ...@@ -224,6 +224,24 @@ export enum TextSearchCompleteMessageType {
Warning = 2, 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. * Information collected when text search is complete.
*/ */
...@@ -244,7 +262,7 @@ export interface TextSearchComplete { ...@@ -244,7 +262,7 @@ export interface TextSearchComplete {
* - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [run a command](command:workbench.action.OpenQuickPick)
* - Click to [open a website](https://aka.ms) * - Click to [open a website](https://aka.ms)
*/ */
message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[]; message?: TextSearchCompleteMessage | TextSearchCompleteMessage[];
} }
/** /**
......
...@@ -153,7 +153,7 @@ export class SearchService extends Disposable implements ISearchService { ...@@ -153,7 +153,7 @@ export class SearchService extends Disposable implements ISearchService {
return { return {
limitHit: completes[0] && completes[0].limitHit, limitHit: completes[0] && completes[0].limitHit,
stats: completes[0].stats, 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)) results: arrays.flatten(completes.map((c: ISearchComplete) => c.results))
}; };
})(); })();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册