提交 7bb8b008 编写于 作者: E Eric Amodio

Fixes #84695 - codicons in hovers

上级 cc70266d
......@@ -15,6 +15,7 @@ import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<string>;
......@@ -118,7 +119,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}
};
renderer.paragraph = (text): string => {
return `<p>${text}</p>`;
return `<p>${markdown.supportThemeIcons ? renderCodicons(text) : text}</p>`;
};
if (options.codeBlockRenderer) {
......@@ -178,7 +179,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
allowedSchemes.push(Schemas.command);
}
const renderedMarkdown = marked.parse(markdown.value, markedOptions);
const renderedMarkdown = marked.parse(
markdown.supportThemeIcons
? markdownEscapeEscapedCodicons(markdown.value)
: markdown.value,
markedOptions
);
element.innerHTML = insane(renderedMarkdown, {
allowedSchemes,
allowedAttributes: {
......
......@@ -6,16 +6,7 @@
import 'vs/css!./codicon/codicon';
import 'vs/css!./codicon/codicon-animations';
import { escape } from 'vs/base/common/strings';
function expand(text: string): string {
return text.replace(/\$\((([a-z0-9\-]+?)(~([a-z0-9\-]*?))?)\)/gi, (_match, _g1, name, _g3, animation) => {
return `<span class="codicon codicon-${name} ${animation ? `codicon-animation-${animation}` : ''}"></span>`;
});
}
export function renderCodicons(label: string): string {
return expand(escape(label));
}
import { renderCodicons } from 'vs/base/common/codicons';
export class CodiconLabel {
......@@ -24,7 +15,7 @@ export class CodiconLabel {
) { }
set text(text: string) {
this._container.innerHTML = renderCodicons(text || '');
this._container.innerHTML = renderCodicons(escape(text ?? ''));
}
set title(title: string) {
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import { renderCodicons } from 'vs/base/browser/ui/codiconLabel/codiconLabel';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
export interface IHighlight {
......@@ -65,13 +65,13 @@ export class HighlightedLabel {
if (pos < highlight.start) {
htmlContent += '<span>';
const substring = this.text.substring(pos, highlight.start);
htmlContent += this.supportCodicons ? renderCodicons(substring) : escape(substring);
htmlContent += this.supportCodicons ? renderCodicons(escape(substring)) : escape(substring);
htmlContent += '</span>';
pos = highlight.end;
}
htmlContent += '<span class="highlight">';
const substring = this.text.substring(highlight.start, highlight.end);
htmlContent += this.supportCodicons ? renderCodicons(substring) : escape(substring);
htmlContent += this.supportCodicons ? renderCodicons(escape(substring)) : escape(substring);
htmlContent += '</span>';
pos = highlight.end;
}
......@@ -79,7 +79,7 @@ export class HighlightedLabel {
if (pos < this.text.length) {
htmlContent += '<span>';
const substring = this.text.substring(pos);
htmlContent += this.supportCodicons ? renderCodicons(substring) : escape(substring);
htmlContent += this.supportCodicons ? renderCodicons(escape(substring)) : escape(substring);
htmlContent += '</span>';
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const escapeCodiconsRegex = /(?<!\\)\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function escapeCodicons(text: string): string {
return text.replace(escapeCodiconsRegex, match => `\\${match}`);
}
const markdownEscapedCodiconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
export function markdownEscapeEscapedCodicons(text: string): string {
// Need to add an extra \ for escaping in markdown
return text.replace(markdownEscapedCodiconsRegex, match => `\\${match}`);
}
const markdownUnescapeCodiconsRegex = /(?<!\\)\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi;
export function markdownUnescapeCodicons(text: string): string {
return text.replace(markdownUnescapeCodiconsRegex, (_, codicon) => `$(${codicon})`);
}
const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi;
export function renderCodicons(text: string): string {
return text.replace(renderCodiconsRegex, (_, escape, codicon, name, animation) => {
return escape
? `$(${codicon})`
: `<span class="codicon codicon-${name}${animation ? ` codicon-animation-${animation}` : ''}"></span>`;
});
}
......@@ -5,37 +5,51 @@
import { equals } from 'vs/base/common/arrays';
import { UriComponents } from 'vs/base/common/uri';
import { escapeCodicons, markdownUnescapeCodicons } from 'vs/base/common/codicons';
export interface IMarkdownString {
readonly value: string;
readonly isTrusted?: boolean;
readonly supportThemeIcons?: boolean;
uris?: { [href: string]: UriComponents };
}
export class MarkdownString implements IMarkdownString {
private readonly _isTrusted: boolean;
private readonly _supportThemeIcons: boolean;
constructor(
private _value: string = '',
isTrustedOrOptions: boolean | { isTrusted?: boolean, supportThemeIcons?: boolean } = false,
) {
if (typeof isTrustedOrOptions === 'boolean') {
this._isTrusted = isTrustedOrOptions;
this._supportThemeIcons = false;
}
else {
this._isTrusted = isTrustedOrOptions.isTrusted ?? false;
this._supportThemeIcons = isTrustedOrOptions.supportThemeIcons ?? false;
}
private _value: string;
private _isTrusted: boolean;
constructor(value: string = '', isTrusted = false) {
this._value = value;
this._isTrusted = isTrusted;
}
get value() { return this._value; }
get isTrusted() { return this._isTrusted; }
get supportThemeIcons() { return this._supportThemeIcons; }
appendText(value: string): MarkdownString {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
this._value += value
value = value
.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&')
.replace('\n', '\n\n');
this._value += this.supportThemeIcons ? markdownUnescapeCodicons(value) : value;
return this;
}
appendMarkdown(value: string): MarkdownString {
this._value += value;
return this;
}
......@@ -47,6 +61,10 @@ export class MarkdownString implements IMarkdownString {
this._value += '\n```\n';
return this;
}
static escapeThemeIcons(value: string): string {
return escapeCodicons(value);
}
}
export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[] | null | undefined): boolean {
......@@ -64,7 +82,8 @@ export function isMarkdownString(thing: any): thing is IMarkdownString {
return true;
} else if (thing && typeof thing === 'object') {
return typeof (<IMarkdownString>thing).value === 'string'
&& (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || (<IMarkdownString>thing).isTrusted === undefined);
&& (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || (<IMarkdownString>thing).isTrusted === undefined)
&& (typeof (<IMarkdownString>thing).supportThemeIcons === 'boolean' || (<IMarkdownString>thing).supportThemeIcons === undefined);
}
return false;
}
......@@ -89,7 +108,7 @@ function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
} else if (!a || !b) {
return false;
} else {
return a.value === b.value && a.isTrusted === b.isTrusted;
return a.value === b.value && a.isTrusted === b.isTrusted && a.supportThemeIcons === b.supportThemeIcons;
}
}
......
......@@ -6,42 +6,104 @@
import * as assert from 'assert';
import * as marked from 'vs/base/common/marked/marked';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { MarkdownString } from 'vs/base/common/htmlContent';
suite('MarkdownRenderer', () => {
test('image rendering conforms to default', () => {
const markdown = { value: `![image](someimageurl 'caption')` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
suite('Images', () => {
test('image rendering conforms to default without title', () => {
const markdown = { value: `![image](someimageurl)` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image rendering conforms to default', () => {
const markdown = { value: `![image](someimageurl 'caption')` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image rendering conforms to default without title', () => {
const markdown = { value: `![image](someimageurl)` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image width from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
});
test('image height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
});
test('image width and height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
});
test('image width from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
});
test('image height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
suite('ThemeIcons Support On', () => {
test('render appendText', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendText('$(zap) $(dont match me)');
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p><span class="codicon codicon-zap"></span> $(dont match me)</p>`);
});
test('render appendText escaped', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendText(MarkdownString.escapeThemeIcons('$(zap) $(dont match me)'));
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p>$(zap) $(dont match me)</p>`);
});
test('render appendMarkdown', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown('$(zap) $(dont match me)');
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p><span class="codicon codicon-zap"></span> $(dont match me)</p>`);
});
test('render appendMarkdown escaped', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown(MarkdownString.escapeThemeIcons('$(zap) $(dont match me)'));
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p>$(zap) $(dont match me)</p>`);
});
});
test('image width and height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
suite('ThemeIcons Support Off', () => {
test('render appendText', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendText('$(zap) $(dont match me)');
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p>$(zap) $(dont match me)</p>`);
});
test('render appendMarkdown', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendMarkdown('$(zap) $(dont match me)');
let result: HTMLElement = renderMarkdown(mds);
assert.strictEqual(result.innerHTML, `<p>$(zap) $(dont match me)</p>`);
});
});
});
......@@ -6,7 +6,7 @@
import * as assert from 'assert';
import { MarkdownString } from 'vs/base/common/htmlContent';
suite('markdownString', () => {
suite('MarkdownString', () => {
test('escape', () => {
......@@ -16,4 +16,63 @@ suite('markdownString', () => {
assert.equal(mds.value, '\\# foo\n\n\\*bar\\*');
});
suite('ThemeIcons', () => {
test('escapeThemeIcons', () => {
assert.equal(
MarkdownString.escapeThemeIcons('$(zap) $(not an icon) foo$(bar)'),
'\\$(zap) $(not an icon) foo\\$(bar)'
);
});
suite('Support On', () => {
test('appendText', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendText('$(zap)');
assert.equal(mds.value, '$(zap)');
});
test('appendText escaped', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendText(MarkdownString.escapeThemeIcons('$(zap)'));
assert.equal(mds.value, '\\\\$\\(zap\\)');
});
test('appendMarkdown', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown('$(zap)');
assert.equal(mds.value, '$(zap)');
});
test('appendMarkdown escaped', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown(MarkdownString.escapeThemeIcons('$(zap)'));
assert.equal(mds.value, '\\$(zap)');
});
});
suite('Support Off', () => {
test('appendText', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendText('$(zap)');
assert.equal(mds.value, '$\\(zap\\)');
});
test('appendMarkdown', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendMarkdown('$(zap)');
assert.equal(mds.value, '$(zap)');
});
});
});
});
......@@ -5,7 +5,8 @@
import 'vs/css!./codelensWidget';
import * as dom from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/ui/codiconLabel/codiconLabel';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
......@@ -88,7 +89,7 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget {
}
hasSymbol = true;
if (lens.command) {
const title = renderCodicons(lens.command.title);
const title = renderCodicons(escape(lens.command.title));
if (lens.command.id) {
innerHtml += `<a id=${i}>${title}</a>`;
this._commands.set(String(i), lens.command);
......
......@@ -380,6 +380,7 @@ declare namespace monaco {
export interface IMarkdownString {
readonly value: string;
readonly isTrusted?: boolean;
readonly supportThemeIcons?: boolean;
uris?: {
[href: string]: UriComponents;
};
......
......@@ -2341,6 +2341,12 @@ declare module 'vscode' {
*/
export class MarkdownString {
/**
* Escapes any [ThemeIcons](#ThemeIcon), e.g. `$(zap)`, in the string.
* @param value A string.
*/
static escapeThemeIcons(value: string): string;
/**
* The markdown string.
*/
......@@ -2356,8 +2362,9 @@ declare module 'vscode' {
* Creates a new markdown string with the given value.
*
* @param value Optional, initial value.
* @param options Optional, options to specify whether [ThemeIcons](#ThemeIcon) are supported within the [`MarkdownString`](#MarkdownString).
*/
constructor(value?: string);
constructor(value?: string, options?: { supportThemeIcons?: boolean });
/**
* Appends and escapes the given string to this markdown string.
......
......@@ -1338,4 +1338,16 @@ declare module 'vscode' {
}
//#endregion
//#region Allow theme icons in hovers: https://github.com/microsoft/vscode/issues/84695
export interface MarkdownString {
/**
* Indicates that this markdown string can contain [ThemeIcons](#ThemeIcon), e.g. `$(zap)`.
*/
readonly supportThemeIcons?: boolean;
}
//#endregion
}
......@@ -309,7 +309,7 @@ export namespace MarkdownString {
}
export function to(value: htmlContent.IMarkdownString): vscode.MarkdownString {
return new htmlContent.MarkdownString(value.value, value.isTrusted);
return new htmlContent.MarkdownString(value.value, { isTrusted: value.isTrusted, supportThemeIcons: value.supportThemeIcons });
}
export function fromStrict(value: string | types.MarkdownString): undefined | string | htmlContent.IMarkdownString {
......
......@@ -14,6 +14,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import * as vscode from 'vscode';
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { markdownUnescapeCodicons, escapeCodicons } from 'vs/base/common/codicons';
function es5ClassCompat(target: Function): any {
///@ts-ignore
......@@ -1231,21 +1232,26 @@ export class MarkdownString {
value: string;
isTrusted?: boolean;
readonly supportThemeIcons?: boolean;
constructor(value?: string) {
this.value = value || '';
constructor(value?: string, { supportThemeIcons }: { supportThemeIcons?: boolean } = {}) {
this.value = value ?? '';
this.supportThemeIcons = supportThemeIcons ?? false;
}
appendText(value: string): MarkdownString {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
this.value += value
value = value
.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&')
.replace('\n', '\n\n');
this.value += this.supportThemeIcons ? markdownUnescapeCodicons(value) : value;
return this;
}
appendMarkdown(value: string): MarkdownString {
this.value += value;
return this;
}
......@@ -1257,6 +1263,10 @@ export class MarkdownString {
this.value += '\n```\n';
return this;
}
static escapeThemeIcons(value: string): string {
return escapeCodicons(value);
}
}
@es5ClassCompat
......
......@@ -38,7 +38,8 @@ import { randomPort } from 'vs/base/node/ports';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderCodicons } from 'vs/base/browser/ui/codiconLabel/codiconLabel';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
......@@ -365,31 +366,31 @@ export class RuntimeExtensionsEditor extends BaseEditor {
if (this._extensionHostProfileService.getUnresponsiveProfile(element.description.identifier)) {
const el = $('span');
el.innerHTML = renderCodicons(` $(alert) Unresponsive`);
el.innerHTML = renderCodicons(escape(` $(alert) Unresponsive`));
el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
data.msgContainer.appendChild(el);
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const el = $('span');
el.innerHTML = renderCodicons(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`);
el.innerHTML = renderCodicons(escape(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
data.msgContainer.appendChild(el);
}
if (element.status.messages && element.status.messages.length > 0) {
const el = $('span');
el.innerHTML = renderCodicons(`$(alert) ${element.status.messages[0].message}`);
el.innerHTML = renderCodicons(escape(`$(alert) ${element.status.messages[0].message}`));
data.msgContainer.appendChild(el);
}
if (element.description.extensionLocation.scheme !== 'file') {
const el = $('span');
el.innerHTML = renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`);
el.innerHTML = renderCodicons(escape(`$(remote) ${element.description.extensionLocation.authority}`));
data.msgContainer.appendChild(el);
const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority);
if (hostLabel) {
el.innerHTML = renderCodicons(`$(remote) ${hostLabel}`);
el.innerHTML = renderCodicons(escape(`$(remote) ${hostLabel}`));
}
}
......
......@@ -25,7 +25,8 @@ import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionba
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { Command } from 'vs/editor/common/modes';
import { renderCodicons } from 'vs/base/browser/ui/codiconLabel/codiconLabel';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IViewDescriptor } from 'vs/workbench/common/views';
......@@ -83,7 +84,7 @@ class StatusBarActionViewItem extends ActionViewItem {
updateLabel(): void {
if (this.options.label && this.label) {
this.label.innerHTML = renderCodicons(this.getAction().label);
this.label.innerHTML = renderCodicons(escape(this.getAction().label));
}
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册