未验证 提交 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 {
readonly title?: string;
}
export interface ILinkOptions {
readonly opener?: (href: string) => void;
}
export interface ILinkStyles {
readonly textLinkForeground?: Color;
readonly disabled?: boolean;
......@@ -33,6 +37,7 @@ export class Link extends Disposable {
constructor(
link: ILinkDescriptor,
options: ILinkOptions | undefined = undefined,
@IOpenerService openerService: IOpenerService
) {
super();
......@@ -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 });
}
}
}));
......
......@@ -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.
*/
......@@ -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[];
}
/**
......
......@@ -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, {});
append(p, link.el);
disposables.add(link);
disposables.add(attachLinkStyler(link, this.themeService));
......
......@@ -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';
......@@ -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';
......@@ -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.$;
......@@ -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,
......@@ -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;
......@@ -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) {
......@@ -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);
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);
this.messageDisposables.add(link);
this.messageDisposables.add(attachLinkStyler(link, this.themeService));
......
......@@ -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.*):$/;
......@@ -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,
......@@ -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;
......@@ -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) {
......@@ -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));
......@@ -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; }
......
......@@ -912,7 +912,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);
......
......@@ -187,7 +187,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));
......@@ -394,7 +394,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));
......
......@@ -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;
}
......@@ -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;
}
......
......@@ -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.
*/
......@@ -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[];
}
/**
......
......@@ -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))
};
})();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册