提交 bcacd2fe 编写于 作者: B Benjamin Pasero

fix #34367

上级 d80bc665
...@@ -9,9 +9,8 @@ import * as DOM from 'vs/base/browser/dom'; ...@@ -9,9 +9,8 @@ import * as DOM from 'vs/base/browser/dom';
import { defaultGenerator } from 'vs/base/common/idGenerator'; import { defaultGenerator } from 'vs/base/common/idGenerator';
import { escape } from 'vs/base/common/strings'; import { escape } from 'vs/base/common/strings';
import { removeMarkdownEscapes, IMarkdownString } from 'vs/base/common/htmlContent'; import { removeMarkdownEscapes, IMarkdownString } from 'vs/base/common/htmlContent';
import { marked, MarkedRenderer, MarkedOptions } from 'vs/base/common/marked/marked'; import { marked, MarkedOptions } from 'vs/base/common/marked/marked';
import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { assign } from 'vs/base/common/objects';
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
export interface IContentActionHandler { export interface IContentActionHandler {
...@@ -25,7 +24,6 @@ export interface RenderOptions { ...@@ -25,7 +24,6 @@ export interface RenderOptions {
actionHandler?: IContentActionHandler; actionHandler?: IContentActionHandler;
codeBlockRenderer?: (modeId: string, value: string) => Thenable<string>; codeBlockRenderer?: (modeId: string, value: string) => Thenable<string>;
codeBlockRenderCallback?: () => void; codeBlockRenderCallback?: () => void;
joinRendererConfiguration?: (renderer: MarkedRenderer) => MarkedOptions;
} }
function createElement(options: RenderOptions): HTMLElement { function createElement(options: RenderOptions): HTMLElement {
...@@ -166,13 +164,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions ...@@ -166,13 +164,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions
renderer renderer
}; };
if (options.joinRendererConfiguration) {
const additionalMarkedOptions = options.joinRendererConfiguration(renderer);
if (additionalMarkedOptions) {
assign(markedOptions, additionalMarkedOptions);
}
}
element.innerHTML = marked(markdown.value, markedOptions); element.innerHTML = marked(markdown.value, markedOptions);
signalInnerHTML(); signalInnerHTML();
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
import Severity from 'vs/base/common/severity'; import Severity from 'vs/base/common/severity';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle'; import { IDisposable } from 'vs/base/common/lifecycle';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IAction } from 'vs/base/common/actions'; import { IAction } from 'vs/base/common/actions';
import Event, { Emitter } from 'vs/base/common/event'; import Event, { Emitter } from 'vs/base/common/event';
...@@ -16,7 +15,7 @@ export import Severity = Severity; ...@@ -16,7 +15,7 @@ export import Severity = Severity;
export const INotificationService = createDecorator<INotificationService>('notificationService'); export const INotificationService = createDecorator<INotificationService>('notificationService');
export type NotificationMessage = string | IMarkdownString | Error; export type NotificationMessage = string | Error;
export interface INotification { export interface INotification {
...@@ -26,11 +25,8 @@ export interface INotification { ...@@ -26,11 +25,8 @@ export interface INotification {
severity: Severity; severity: Severity;
/** /**
* The message of the notification. This can either be a `string`, `Error` * The message of the notification. This can either be a `string` or `Error`. Messages
* or `IMarkdownString`. * can optionally include links in the format: `[text](link)`
*
* **Note:** Currently only links are supported in notifications. Links to commands can
* be embedded provided that the `IMarkdownString` is trusted.
*/ */
message: NotificationMessage; message: NotificationMessage;
......
...@@ -6,8 +6,7 @@ ...@@ -6,8 +6,7 @@
'use strict'; 'use strict';
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { renderMarkdown, IContentActionHandler } from 'vs/base/browser/htmlContentRenderer'; import { clearNode, addClass, removeClass, toggleClass, addDisposableListener } from 'vs/base/browser/dom';
import { clearNode, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
import URI from 'vs/base/common/uri'; import URI from 'vs/base/common/uri';
import { onUnexpectedError } from 'vs/base/common/errors'; import { onUnexpectedError } from 'vs/base/common/errors';
...@@ -15,17 +14,15 @@ import { localize } from 'vs/nls'; ...@@ -15,17 +14,15 @@ import { localize } from 'vs/nls';
import { ButtonGroup } from 'vs/base/browser/ui/button/button'; import { ButtonGroup } from 'vs/base/browser/ui/button/button';
import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, IActionRunner } from 'vs/base/common/actions'; import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications'; import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage } from 'vs/workbench/common/notifications';
import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { MarkedOptions } from 'vs/base/common/marked/marked';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { Severity } from 'vs/platform/notification/common/notification'; import { Severity } from 'vs/platform/notification/common/notification';
...@@ -92,8 +89,8 @@ export class NotificationsListDelegate implements IDelegate<INotificationViewIte ...@@ -92,8 +89,8 @@ export class NotificationsListDelegate implements IDelegate<INotificationViewIte
} }
this.offsetHelper.style.width = `calc(100% - ${10 /* padding */ + 24 /* severity icon */ + (actions * 24) /* 24px per action */}px)`; this.offsetHelper.style.width = `calc(100% - ${10 /* padding */ + 24 /* severity icon */ + (actions * 24) /* 24px per action */}px)`;
// Render message markdown into offset helper // Render message into offset helper
const renderedMessage = NotificationMessageMarkdownRenderer.render(notification.message); const renderedMessage = NotificationMessageRenderer.render(notification.message);
this.offsetHelper.appendChild(renderedMessage); this.offsetHelper.appendChild(renderedMessage);
// Compute height // Compute height
...@@ -131,30 +128,49 @@ export interface INotificationTemplateData { ...@@ -131,30 +128,49 @@ export interface INotificationTemplateData {
renderer: NotificationTemplateRenderer; renderer: NotificationTemplateRenderer;
} }
class NotificationMessageMarkdownRenderer { interface IMessageActionHandler {
callback: (href: string) => void;
disposeables: IDisposable[];
}
private static readonly MARKED_NOOP = (text?: string) => text || ''; class NotificationMessageRenderer {
private static readonly MARKED_NOOP_TARGETS = [
'blockquote', 'br', 'code', 'codespan', 'del', 'em', 'heading', 'hr', 'html',
'image', 'list', 'listitem', 'paragraph', 'strong', 'table', 'tablecell',
'tablerow'
];
public static render(markdown: IMarkdownString, actionHandler?: IContentActionHandler): HTMLElement { public static render(message: INotificationMessage, actionHandler?: IMessageActionHandler): HTMLElement {
return renderMarkdown(markdown, { const messageContainer = document.createElement('span');
inline: true,
joinRendererConfiguration: renderer => {
// Overwrite markdown render functions as no-ops // Message has no links
NotificationMessageMarkdownRenderer.MARKED_NOOP_TARGETS.forEach(fn => renderer[fn] = NotificationMessageMarkdownRenderer.MARKED_NOOP); if (message.links.length === 0) {
messageContainer.textContent = message.value;
}
return { // Message has links
gfm: false, // disable GitHub style markdown, else {
smartypants: false // disable some text transformations let index = 0;
} as MarkedOptions; let textBefore: string;
}, for (let i = 0; i < message.links.length; i++) {
actionHandler const link = message.links[i];
});
textBefore = message.value.substring(index, link.offset);
if (textBefore) {
messageContainer.appendChild(document.createTextNode(textBefore));
}
const anchor = document.createElement('a');
anchor.textContent = link.name;
anchor.title = link.href;
anchor.href = link.href;
if (actionHandler) {
actionHandler.disposeables.push(addDisposableListener(anchor, 'click', () => actionHandler.callback(link.href)));
}
messageContainer.appendChild(anchor);
index = link.offset + link.length;
}
}
return messageContainer;
} }
} }
...@@ -340,8 +356,8 @@ export class NotificationTemplateRenderer { ...@@ -340,8 +356,8 @@ export class NotificationTemplateRenderer {
private renderMessage(notification: INotificationViewItem): boolean { private renderMessage(notification: INotificationViewItem): boolean {
clearNode(this.template.message); clearNode(this.template.message);
this.template.message.appendChild(NotificationMessageMarkdownRenderer.render(notification.message, { this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, {
callback: (content: string) => this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError), callback: link => this.openerService.open(URI.parse(link)).then(void 0, onUnexpectedError),
disposeables: this.inputDisposeables disposeables: this.inputDisposeables
})); }));
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
'use strict'; 'use strict';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage } from 'vs/platform/notification/common/notification'; import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage'; import { toErrorMessage } from 'vs/base/common/errorMessage';
import Event, { Emitter, once } from 'vs/base/common/event'; import Event, { Emitter, once } from 'vs/base/common/event';
...@@ -184,7 +183,7 @@ export class NotificationsModel implements INotificationsModel { ...@@ -184,7 +183,7 @@ export class NotificationsModel implements INotificationsModel {
export interface INotificationViewItem { export interface INotificationViewItem {
readonly severity: Severity; readonly severity: Severity;
readonly message: IMarkdownString; readonly message: INotificationMessage;
readonly source: string; readonly source: string;
readonly actions: INotificationActions; readonly actions: INotificationActions;
readonly progress: INotificationViewItemProgress; readonly progress: INotificationViewItemProgress;
...@@ -320,10 +319,27 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre ...@@ -320,10 +319,27 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre
} }
} }
export interface IMessageLink {
name: string;
href: string;
offset: number;
length: number;
}
export interface INotificationMessage {
raw: string;
value: string;
links: IMessageLink[];
}
export class NotificationViewItem implements INotificationViewItem { export class NotificationViewItem implements INotificationViewItem {
private static MAX_MESSAGE_LENGTH = 1000; private static MAX_MESSAGE_LENGTH = 1000;
// Example link: "Some message with [link text](http://link.href)."
// RegEx: [, anything not ], ], (, http:|https:, //, no whitespace)
private static LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^\)\s]+)\)/gi;
private _expanded: boolean; private _expanded: boolean;
private toDispose: IDisposable[]; private toDispose: IDisposable[];
...@@ -346,15 +362,11 @@ export class NotificationViewItem implements INotificationViewItem { ...@@ -346,15 +362,11 @@ export class NotificationViewItem implements INotificationViewItem {
severity = Severity.Info; severity = Severity.Info;
} }
const message = NotificationViewItem.toMarkdownString(notification.message); const message = NotificationViewItem.parseNotificationMessage(notification.message);
if (!message) { if (!message) {
return null; // we need a message to show return null; // we need a message to show
} }
if (message.value.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
message.value = `${message.value.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
}
let actions: INotificationActions; let actions: INotificationActions;
if (notification.actions) { if (notification.actions) {
actions = notification.actions; actions = notification.actions;
...@@ -365,21 +377,42 @@ export class NotificationViewItem implements INotificationViewItem { ...@@ -365,21 +377,42 @@ export class NotificationViewItem implements INotificationViewItem {
return new NotificationViewItem(severity, message, notification.source, actions); return new NotificationViewItem(severity, message, notification.source, actions);
} }
private static toMarkdownString(input: NotificationMessage): IMarkdownString { private static parseNotificationMessage(input: NotificationMessage): INotificationMessage {
let message: IMarkdownString; let message: string;
if (input instanceof Error) { if (input instanceof Error) {
message = { value: toErrorMessage(input, false), isTrusted: false }; message = toErrorMessage(input, false);
} else if (typeof input === 'string') { } else if (typeof input === 'string') {
message = { value: input, isTrusted: false };
} else if (input.value && typeof input.value === 'string') {
message = input; message = input;
} }
return message; if (!message) {
return null; // we need a message to show
}
const raw = message;
// Make sure message is in the limits
if (message.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
message = `${message.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
}
// Remove newlines from messages as we do not support that and it makes link parsing hard
message = message.replace(/(\r\n|\n|\r)/gm, ' ').trim();
// Parse Links
const links: IMessageLink[] = [];
message.replace(NotificationViewItem.LINK_REGEX, (matchString: string, name: string, href: string, offset: number) => {
links.push({ name, href, offset, length: matchString.length });
return matchString;
});
return { raw, value: message, links };
} }
private constructor(private _severity: Severity, private _message: IMarkdownString, private _source: string, actions?: INotificationActions) { private constructor(private _severity: Severity, private _message: INotificationMessage, private _source: string, actions?: INotificationActions) {
this.toDispose = []; this.toDispose = [];
this.setActions(actions); this.setActions(actions);
...@@ -452,7 +485,7 @@ export class NotificationViewItem implements INotificationViewItem { ...@@ -452,7 +485,7 @@ export class NotificationViewItem implements INotificationViewItem {
return this._progress; return this._progress;
} }
public get message(): IMarkdownString { public get message(): INotificationMessage {
return this._message; return this._message;
} }
...@@ -470,7 +503,7 @@ export class NotificationViewItem implements INotificationViewItem { ...@@ -470,7 +503,7 @@ export class NotificationViewItem implements INotificationViewItem {
} }
public updateMessage(input: NotificationMessage): void { public updateMessage(input: NotificationMessage): void {
const message = NotificationViewItem.toMarkdownString(input); const message = NotificationViewItem.parseNotificationMessage(input);
if (!message) { if (!message) {
return; return;
} }
......
...@@ -18,7 +18,6 @@ suite('Notifications', () => { ...@@ -18,7 +18,6 @@ suite('Notifications', () => {
// Invalid // Invalid
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: '' })); assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: '' }));
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: null })); assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: null }));
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: { value: '', isTrusted: true } }));
// Duplicates // Duplicates
let item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }); let item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' });
...@@ -107,6 +106,21 @@ suite('Notifications', () => { ...@@ -107,6 +106,21 @@ suite('Notifications', () => {
// Error with Action // Error with Action
let item6 = NotificationViewItem.create({ severity: Severity.Error, message: create('Hello Error', { actions: [new Action('id', 'label')] }) }); let item6 = NotificationViewItem.create({ severity: Severity.Error, message: create('Hello Error', { actions: [new Action('id', 'label')] }) });
assert.equal(item6.actions.primary.length, 1); assert.equal(item6.actions.primary.length, 1);
// Links
let item7 = NotificationViewItem.create({ severity: Severity.Info, message: 'Unable to [Link 1](http://link1.com) open [Link 2](https://link2.com) and [Invalid Link3](ftp://link3.com)' });
const links = item7.message.links;
assert.equal(links.length, 2);
assert.equal(links[0].name, 'Link 1');
assert.equal(links[0].href, 'http://link1.com');
assert.equal(links[0].length, '[Link 1](http://link1.com)'.length);
assert.equal(links[0].offset, 'Unable to '.length);
assert.equal(links[1].name, 'Link 2');
assert.equal(links[1].href, 'https://link2.com');
assert.equal(links[1].length, '[Link 2](https://link2.com)'.length);
assert.equal(links[1].offset, 'Unable to [Link 1](http://link1.com) open '.length);
}); });
test('Model', () => { test('Model', () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册