From 9791d58701e35e0d106159625b6f76ca770dd0e3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 23 Aug 2017 15:53:21 +0200 Subject: [PATCH] replace MarkedString with object-type IMarkdownString, #29076 --- build/monaco/monaco.d.ts.recipe | 2 +- src/vs/base/browser/htmlContentRenderer.ts | 18 ++--- src/vs/base/common/htmlContent.ts | 71 +++++++++++++++---- src/vs/base/test/browser/htmlContent.test.ts | 16 ++--- src/vs/editor/common/editorCommon.ts | 10 +-- .../common/model/textModelWithDecorations.ts | 8 +-- src/vs/editor/common/modes.ts | 4 +- .../common/services/modelServiceImpl.ts | 6 +- .../browser/goToDeclarationMouse.ts | 11 +-- .../hover/browser/modesContentHover.ts | 14 ++-- .../contrib/hover/browser/modesGlyphHover.ts | 16 ++--- src/vs/editor/contrib/links/browser/links.ts | 9 +-- .../quickFix/browser/lightBulbWidget.ts | 5 +- .../common/model/modelDecorations.test.ts | 6 +- src/vs/monaco.d.ts | 18 +++-- .../api/node/extHostLanguageFeatures.ts | 2 +- .../api/node/extHostTypeConverters.ts | 12 ++-- .../debug/browser/debugEditorModelManager.ts | 13 ++-- .../browser/keybindingsEditorContribution.ts | 14 ++-- .../browser/preferencesRenderers.ts | 5 +- .../preferences/browser/preferencesWidgets.ts | 5 +- .../api/extHostLanguageFeatures.test.ts | 4 +- 22 files changed, 160 insertions(+), 109 deletions(-) diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 9c2970d508b..cdaee801a9c 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -36,7 +36,7 @@ declare module monaco { #include(vs/base/common/cancellation): CancellationTokenSource, CancellationToken #include(vs/base/common/uri): URI #include(vs/editor/common/standalone/standaloneBase): KeyCode, KeyMod -#include(vs/base/common/htmlContent): MarkedString +#include(vs/base/common/htmlContent): IMarkdownString #include(vs/base/browser/keyboardEvent): IKeyboardEvent #include(vs/base/browser/mouseEvent): IMouseEvent #include(vs/editor/common/editorCommon): IScrollEvent diff --git a/src/vs/base/browser/htmlContentRenderer.ts b/src/vs/base/browser/htmlContentRenderer.ts index 6dd423dbec1..0da820a99f9 100644 --- a/src/vs/base/browser/htmlContentRenderer.ts +++ b/src/vs/base/browser/htmlContentRenderer.ts @@ -10,7 +10,7 @@ import * as DOM from 'vs/base/browser/dom'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { escape } from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MarkedString, removeMarkdownEscapes } from 'vs/base/common/htmlContent'; +import { removeMarkdownEscapes, IMarkdownString } from 'vs/base/common/htmlContent'; import { marked } from 'vs/base/common/marked/marked'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -30,14 +30,6 @@ function createElement(options: RenderOptions): HTMLElement { return element; } - -export function renderMarkedString(markedString: MarkedString, options: RenderOptions = {}): Node { - // this is sort of legacy given that we have full - // support for markdown. Turn this into markdown - // and continue - return renderMarkdown(markedString, options); -} - export function renderText(text: string, options: RenderOptions = {}): Node { const element = createElement(options); element.textContent = text; @@ -56,7 +48,7 @@ export function renderFormattedText(formattedText: string, options: RenderOption * @param content a html element description * @param actionCallback a callback function for any action links in the string. Argument is the zero-based index of the clicked action. */ -export function renderMarkdown(markdown: string, options: RenderOptions = {}): Node { +export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): Node { const element = createElement(options); const { codeBlockRenderer, actionCallback } = options; @@ -113,7 +105,9 @@ export function renderMarkdown(markdown: string, options: RenderOptions = {}): N if (!href || href.match(/^data:|javascript:/i)) { return text; } else if (href.match(/^command:/i)) { - return `${text} `; + return markdown.enableCommands + ? `${text} ` + : text; } else { return `${text}`; @@ -161,7 +155,7 @@ export function renderMarkdown(markdown: string, options: RenderOptions = {}): N }); } - element.innerHTML = marked(markdown, { + element.innerHTML = marked(markdown.value, { sanitize: true, renderer }); diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index c758efe59c8..f047d7effce 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -8,28 +8,75 @@ import { equals } from 'vs/base/common/arrays'; import { marked } from 'vs/base/common/marked/marked'; -/** - * MarkedString can be used to render human readable text. It is either a markdown string - * or a code-block that provides a language and a code snippet. Note that - * markdown strings will be sanitized - that means html will be escaped. - */ -export type MarkedString = string; +export interface IMarkdownString { + value: string; + enableCommands?: true; +} -export function markedStringsEquals(a: MarkedString | MarkedString[], b: MarkedString | MarkedString[]): boolean { +export class MarkdownString implements IMarkdownString { + + static isMarkdownString(thing: any): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; + } else if (typeof thing === 'object') { + return typeof (thing).value === 'string' + && (typeof (thing).enableCommands === 'boolean' || (thing).enableCommands === void 0); + } + return false; + } + + value: string; + enableCommands?: true; + + constructor(value: string = '') { + this.value = value; + } + + appendText(value: string): this { + this.value += textToMarkedString(value); + return this; + } + + // appendMarkdown(value: string): this { + // this.value += value; + // return this; + // } + + appendCodeblock(langId: string, code: string): this { + this.value += '```'; + this.value += langId; + this.value += '\n'; + this.value += code; + this.value += '```\n'; + return this; + } +} + +export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean { if (!a && !b) { return true; } else if (!a || !b) { return false; - } else if (typeof a === 'string' && typeof b === 'string') { - return a === b; } else if (Array.isArray(a) && Array.isArray(b)) { - return equals(a, b); + return equals(a, b, markdownStringEqual); + } else if (MarkdownString.isMarkdownString(a) && MarkdownString.isMarkdownString(b)) { + return markdownStringEqual(a, b); } else { return false; } } -export function textToMarkedString(text: string): MarkedString { +function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.value === b.value && a.enableCommands === b.enableCommands; + } +} + +export function textToMarkedString(text: string): string { return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash } @@ -40,7 +87,7 @@ export function removeMarkdownEscapes(text: string): string { return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); } -export function containsCommandLink(value: MarkedString): boolean { +export function containsCommandLink(value: string): boolean { let uses = false; const renderer = new marked.Renderer(); renderer.link = (href, title, text): string => { diff --git a/src/vs/base/test/browser/htmlContent.test.ts b/src/vs/base/test/browser/htmlContent.test.ts index 537777365e4..dcd1c4d5afb 100644 --- a/src/vs/base/test/browser/htmlContent.test.ts +++ b/src/vs/base/test/browser/htmlContent.test.ts @@ -87,35 +87,35 @@ suite('HtmlContent', () => { assert.strictEqual(result.innerHTML, '**bold**'); }); test('image rendering conforms to default', () => { - const markdown = `![image](someimageurl 'caption')`; + const markdown = { value: `![image](someimageurl 'caption')` }; const result: HTMLElement = renderMarkdown(markdown); const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown, { + const imageFromMarked = marked(markdown.value, { sanitize: true, renderer }).trim(); assert.strictEqual(result.innerHTML, imageFromMarked); }); test('image rendering conforms to default without title', () => { - const markdown = `![image](someimageurl)`; + const markdown = { value: `![image](someimageurl)` }; const result: HTMLElement = renderMarkdown(markdown); const renderer = new marked.Renderer(); - const imageFromMarked = marked(markdown, { + const imageFromMarked = marked(markdown.value, { sanitize: true, renderer }).trim(); assert.strictEqual(result.innerHTML, imageFromMarked); }); test('image width from title params', () => { - var result: HTMLElement = renderMarkdown(`![image](someimageurl|width=100 'caption')`); + var result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` }); assert.strictEqual(result.innerHTML, `

image

`); }); test('image height from title params', () => { - var result: HTMLElement = renderMarkdown(`![image](someimageurl|height=100 'caption')`); + var result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` }); assert.strictEqual(result.innerHTML, `

image

`); }); test('image width and height from title params', () => { - var result: HTMLElement = renderMarkdown(`![image](someimageurl|height=200,width=100 'caption')`); + var result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` }); assert.strictEqual(result.innerHTML, `

image

`); }); -}); \ No newline at end of file +}); diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index b2a8bde84ba..be008c50178 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -5,7 +5,7 @@ 'use strict'; import { BulkListenerCallback } from 'vs/base/common/eventEmitter'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -77,11 +77,11 @@ export interface IModelDecorationOptions { /** * Message to be rendered when hovering over the glyph margin decoration. */ - glyphMarginHoverMessage?: MarkedString | MarkedString[]; + glyphMarginHoverMessage?: IMarkdownString | IMarkdownString[]; /** - * Array of MarkedString to render as the decoration message. + * Array of MarkdownString to render as the decoration message. */ - hoverMessage?: MarkedString | MarkedString[]; + hoverMessage?: IMarkdownString | IMarkdownString[]; /** * Should the decoration expand to encompass a whole line. */ @@ -1728,7 +1728,7 @@ export interface IDecorationInstanceRenderOptions extends IThemeDecorationInstan */ export interface IDecorationOptions { range: IRange; - hoverMessage?: MarkedString | MarkedString[]; + hoverMessage?: IMarkdownString | IMarkdownString[]; renderOptions?: IDecorationInstanceRenderOptions; } diff --git a/src/vs/editor/common/model/textModelWithDecorations.ts b/src/vs/editor/common/model/textModelWithDecorations.ts index 932d6a2c6ec..d9f84771f84 100644 --- a/src/vs/editor/common/model/textModelWithDecorations.ts +++ b/src/vs/editor/common/model/textModelWithDecorations.ts @@ -5,7 +5,7 @@ 'use strict'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { MarkedString, markedStringsEquals } from 'vs/base/common/htmlContent'; +import { IMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; import * as strings from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { Range, IRange } from 'vs/editor/common/core/range'; @@ -894,8 +894,8 @@ export class ModelDecorationOptions implements editorCommon.IModelDecorationOpti readonly staticId: number; readonly stickiness: editorCommon.TrackedRangeStickiness; readonly className: string; - readonly hoverMessage: MarkedString | MarkedString[]; - readonly glyphMarginHoverMessage: MarkedString | MarkedString[]; + readonly hoverMessage: IMarkdownString | IMarkdownString[]; + readonly glyphMarginHoverMessage: IMarkdownString | IMarkdownString[]; readonly isWholeLine: boolean; readonly showIfCollapsed: boolean; readonly overviewRuler: ModelDecorationOverviewRulerOptions; @@ -911,7 +911,7 @@ export class ModelDecorationOptions implements editorCommon.IModelDecorationOpti this.stickiness = options.stickiness || editorCommon.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; this.className = options.className ? cleanClassName(options.className) : strings.empty; this.hoverMessage = options.hoverMessage || []; - this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || strings.empty; + this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || []; this.isWholeLine = options.isWholeLine || false; this.showIfCollapsed = options.showIfCollapsed || false; this.overviewRuler = new ModelDecorationOverviewRulerOptions(options.overviewRuler); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 3e82ee59652..4d7bebc731d 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; @@ -160,7 +160,7 @@ export interface Hover { /** * The contents of this hover. */ - contents: MarkedString[]; + contents: IMarkdownString[]; /** * The range to which this hover applies. When missing, the diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 14e90ee2cfb..20565a52a3f 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import network = require('vs/base/common/network'); import Event, { Emitter } from 'vs/base/common/event'; import { EmitterEvent } from 'vs/base/common/eventEmitter'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import URI from 'vs/base/common/uri'; @@ -136,7 +136,7 @@ class ModelMarkerHandler { break; } - let hoverMessage: MarkedString[] = null; + let hoverMessage: IMarkdownString = null; let { message, source } = marker; if (typeof message === 'string') { @@ -150,7 +150,7 @@ class ModelMarkerHandler { } } - hoverMessage = ['```_\n' + message + '\n```']; + hoverMessage = { value: '```_\n' + message + '\n```' }; } return { diff --git a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclarationMouse.ts b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclarationMouse.ts index f78d1b59cef..bb47505032e 100644 --- a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclarationMouse.ts +++ b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclarationMouse.ts @@ -9,7 +9,7 @@ import 'vs/css!./goToDeclarationMouse'; import * as nls from 'vs/nls'; import { Throttler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Range } from 'vs/editor/common/core/range'; @@ -112,7 +112,10 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC // Multiple results if (results.length > 1) { - this.addDecoration(new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn), nls.localize('multipleResults', "Click to show {0} definitions.", results.length)); + this.addDecoration( + new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn), + new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length)) + ); } // Single result @@ -156,7 +159,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC this.addDecoration( new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn), - '```' + this.modeService.getModeIdByFilenameOrFirstLine(textEditorModel.uri.fsPath) + '\n' + value + '\n```' + new MarkdownString().appendCodeblock(this.modeService.getModeIdByFilenameOrFirstLine(textEditorModel.uri.fsPath), value) ); ref.dispose(); }); @@ -164,7 +167,7 @@ class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorC }).done(undefined, onUnexpectedError); } - private addDecoration(range: Range, hoverMessage: MarkedString): void { + private addDecoration(range: Range, hoverMessage: MarkdownString): void { const newDecorations: editorCommon.IModelDeltaDecoration = { range: range, diff --git a/src/vs/editor/contrib/hover/browser/modesContentHover.ts b/src/vs/editor/contrib/hover/browser/modesContentHover.ts index 323f55d5fc6..a8119850568 100644 --- a/src/vs/editor/contrib/hover/browser/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/browser/modesContentHover.ts @@ -9,7 +9,7 @@ import URI from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as dom from 'vs/base/browser/dom'; import { TPromise } from 'vs/base/common/winjs.base'; -import { renderMarkedString } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -20,7 +20,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { getHover } from '../common/hover'; import { HoverOperation, IHoverComputer } from './hoverOperation'; import { ContentHoverWidget } from './hoverWidgets'; -import { textToMarkedString, MarkedString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel'; import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget'; @@ -81,8 +81,8 @@ class ModesContentComputer implements IHoverComputer { return []; } - const hasHoverContent = (contents: MarkedString | MarkedString[]) => { - return contents && (!Array.isArray(contents) || (contents).length > 0); + const hasHoverContent = (contents: IMarkdownString | IMarkdownString[]) => { + return contents && (!Array.isArray(contents) || (contents).length > 0); }; const colorDetector = ColorDetector.get(this._editor); @@ -111,7 +111,7 @@ class ModesContentComputer implements IHoverComputer { return null; } - let contents: MarkedString[]; + let contents: IMarkdownString[]; if (d.options.hoverMessage) { if (Array.isArray(d.options.hoverMessage)) { @@ -155,7 +155,7 @@ class ModesContentComputer implements IHoverComputer { private _getLoadingMessage(): HoverPart { return { range: this._range, - contents: [textToMarkedString(nls.localize('modesContentHover.loading', "Loading..."))] + contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))] }; } } @@ -316,7 +316,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { msg.contents .filter(contents => !!contents) .forEach(contents => { - const renderedContents = renderMarkedString(contents, { + const renderedContents = renderMarkdown(contents, { actionCallback: (content) => { this._openerService.open(URI.parse(content)).then(void 0, onUnexpectedError); }, diff --git a/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts b/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts index 0900532ea51..7fdd61c2b15 100644 --- a/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/browser/modesGlyphHover.ts @@ -8,17 +8,17 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { HoverOperation, IHoverComputer } from './hoverOperation'; import { GlyphHoverWidget } from './hoverWidgets'; import { $ } from 'vs/base/browser/dom'; -import { renderMarkedString } from 'vs/base/browser/htmlContentRenderer'; +import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import URI from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModeService } from 'vs/editor/common/services/modeService'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface IHoverMessage { - value: MarkedString; + value: IMarkdownString; } class MarginComputer implements IHoverComputer { @@ -42,10 +42,10 @@ class MarginComputer implements IHoverComputer { } public computeSync(): IHoverMessage[] { - const hasHoverContent = (contents: MarkedString | MarkedString[]) => { - return contents && (!Array.isArray(contents) || (contents).length > 0); + const hasHoverContent = (contents: IMarkdownString | IMarkdownString[]) => { + return contents && (!Array.isArray(contents) || (contents).length > 0); }; - const toHoverMessage = (contents: MarkedString): IHoverMessage => { + const toHoverMessage = (contents: IMarkdownString): IHoverMessage => { return { value: contents }; @@ -168,7 +168,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { const fragment = document.createDocumentFragment(); messages.forEach((msg) => { - const renderedContents = renderMarkedString(msg.value, { + const renderedContents = renderMarkdown(msg.value, { actionCallback: content => this.openerService.open(URI.parse(content)).then(undefined, onUnexpectedError), codeBlockRenderer: (languageAlias, value): string | TPromise => { // In markdown, it is possible that we stumble upon language aliases (e.g. js instead of javascript) @@ -185,4 +185,4 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { this.updateContents(fragment); this.showAt(lineNumber); } -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/links/browser/links.ts b/src/vs/editor/contrib/links/browser/links.ts index 53ed5659397..4c6eceb7ff6 100644 --- a/src/vs/editor/contrib/links/browser/links.ts +++ b/src/vs/editor/contrib/links/browser/links.ts @@ -26,21 +26,22 @@ import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegist import { Position } from 'vs/editor/common/core/position'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'vs/editor/contrib/goToDeclaration/browser/clickLinkGesture'; +import { MarkdownString } from 'vs/base/common/htmlContent'; -const HOVER_MESSAGE_GENERAL_META = ( +const HOVER_MESSAGE_GENERAL_META = new MarkdownString().appendText( platform.isMacintosh ? nls.localize('links.navigate.mac', "Cmd + click to follow link") : nls.localize('links.navigate', "Ctrl + click to follow link") ); -const HOVER_MESSAGE_COMMAND_META = ( +const HOVER_MESSAGE_COMMAND_META = new MarkdownString().appendText( platform.isMacintosh ? nls.localize('links.command.mac', "Cmd + click to execute command") : nls.localize('links.command', "Ctrl + click to execute command") ); -const HOVER_MESSAGE_GENERAL_ALT = nls.localize('links.navigate.al', "Alt + click to follow link"); -const HOVER_MESSAGE_COMMAND_ALT = nls.localize('links.command.al', "Alt + click to execute command"); +const HOVER_MESSAGE_GENERAL_ALT = new MarkdownString().appendText(nls.localize('links.navigate.al', "Alt + click to follow link")); +const HOVER_MESSAGE_COMMAND_ALT = new MarkdownString().appendText(nls.localize('links.command.al', "Alt + click to execute command")); const decoration = { meta: ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/quickFix/browser/lightBulbWidget.ts b/src/vs/editor/contrib/quickFix/browser/lightBulbWidget.ts index c1bde40b2d7..3db81fda26c 100644 --- a/src/vs/editor/contrib/quickFix/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/quickFix/browser/lightBulbWidget.ts @@ -12,13 +12,14 @@ import * as dom from 'vs/base/browser/dom'; import { TrackedRangeStickiness } from 'vs/editor/common/editorCommon'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { QuickFixComputeEvent } from './quickFixModel'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export class LightBulbWidget implements IDisposable { private readonly _options = { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, glyphMarginClassName: 'lightbulb-glyph', - glyphMarginHoverMessage: undefined + glyphMarginHoverMessage: undefined }; private readonly _editor: ICodeEditor; @@ -90,7 +91,7 @@ export class LightBulbWidget implements IDisposable { } get title() { - return this._options.glyphMarginHoverMessage; + return this._options.glyphMarginHoverMessage && this._options.glyphMarginHoverMessage.value; } show(e: QuickFixComputeEvent): void { diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index b148ac36e17..1b2264090f0 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -542,7 +542,7 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { - hoverMessage: 'hello1' + hoverMessage: { value: 'hello1' } } }]); @@ -554,13 +554,13 @@ suite('deltaDecorations', () => { endColumn: 1 }, options: { - hoverMessage: 'hello2' + hoverMessage: { value: 'hello2' } } }]); let actualDecoration = model.getDecorationOptions(ids[0]); - assert.equal(actualDecoration.hoverMessage, 'hello2'); + assert.deepEqual(actualDecoration.hoverMessage, { value: 'hello2' }); model.dispose(); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 9b898e112be..3cc8caa67f8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -353,12 +353,10 @@ declare module monaco { static readonly WinCtrl: number; static chord(firstPart: number, secondPart: number): number; } - /** - * MarkedString can be used to render human readable text. It is either a markdown string - * or a code-block that provides a language and a code snippet. Note that - * markdown strings will be sanitized - that means html will be escaped. - */ - export type MarkedString = string; + export interface IMarkdownString { + value: string; + enableCommands?: true; + } export interface IKeyboardEvent { readonly browserEvent: KeyboardEvent; @@ -1115,11 +1113,11 @@ declare module monaco.editor { /** * Message to be rendered when hovering over the glyph margin decoration. */ - glyphMarginHoverMessage?: MarkedString | MarkedString[]; + glyphMarginHoverMessage?: IMarkdownString | IMarkdownString[]; /** - * Array of MarkedString to render as the decoration message. + * Array of MarkdownString to render as the decoration message. */ - hoverMessage?: MarkedString | MarkedString[]; + hoverMessage?: IMarkdownString | IMarkdownString[]; /** * Should the decoration expand to encompass a whole line. */ @@ -4410,7 +4408,7 @@ declare module monaco.languages { /** * The contents of this hover. */ - contents: MarkedString[]; + contents: IMarkdownString[]; /** * The range to which this hover applies. When missing, the * editor will use the range at the current position or the diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index f6049cdab17..3e295379597 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -204,7 +204,7 @@ class HoverAdapter { // we wanna know which extension uses command links // because that is a potential trick-attack on users - if (result.contents.some(containsCommandLink)) { + if (result.contents.some(h => containsCommandLink(h.value))) { this._telemetryLog('usesCommandLink', { from: 'hover' }); } return result; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index a0ea9970792..da4e8544b50 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -16,6 +16,7 @@ import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface PositionLike { line: number; @@ -144,14 +145,17 @@ function isDecorationOptionsArr(something: vscode.Range[] | vscode.DecorationOpt } export namespace MarkedString { - export function from(markup: vscode.MarkedString): string { + export function from(markup: vscode.MarkedString): IMarkdownString { if (typeof markup === 'string' || !markup) { - return markup; + return { value: markup, enableCommands: true }; } else { const { language, value } = markup; - return '```' + language + '\n' + value + '\n```'; + return { value: '```' + language + '\n' + value + '\n```' }; } } + export function to(value: IMarkdownString): vscode.MarkedString { + return value.value; + } } export function fromRangeOrRangeWithMessage(ranges: vscode.Range[] | vscode.DecorationOptions[]): IDecorationOptions[] { @@ -272,7 +276,7 @@ export function fromHover(hover: vscode.Hover): modes.Hover { } export function toHover(info: modes.Hover): types.Hover { - return new types.Hover(info.contents, toRange(info.range)); + return new types.Hover(info.contents.map(MarkedString.to), toRange(info.range)); } export function toDocumentHighlight(occurrence: modes.DocumentHighlight): types.DocumentHighlight { diff --git a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 63db8dbc424..e229724a162 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -14,6 +14,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDebugService, IBreakpoint, IRawBreakpoint, State } from 'vs/workbench/parts/debug/common/debug'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { MarkdownString } from 'vs/base/common/htmlContent'; interface IDebugEditorModelData { model: IModel; @@ -284,7 +285,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { if (result) { result = objects.clone(result); if (breakpoint.message) { - result.glyphMarginHoverMessage = breakpoint.message; + result.glyphMarginHoverMessage = new MarkdownString().appendText(breakpoint.message); } if (breakpoint.column) { result.beforeContentClassName = `debug-breakpoint-column ${result.glyphMarginClassName}-column`; @@ -305,7 +306,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { } else { condition = breakpoint.condition ? breakpoint.condition : breakpoint.hitCondition; } - const glyphMarginHoverMessage = `\`\`\`${modeId}\n${condition}\`\`\``; + const glyphMarginHoverMessage = new MarkdownString().appendCodeblock(modeId, condition); const glyphMarginClassName = 'debug-breakpoint-conditional-glyph'; const beforeContentClassName = breakpoint.column ? `debug-breakpoint-column ${glyphMarginClassName}-column` : undefined; @@ -326,25 +327,25 @@ export class DebugEditorModelManager implements IWorkbenchContribution { private static BREAKPOINT_DISABLED_DECORATION: IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-disabled-glyph', - glyphMarginHoverMessage: nls.localize('breakpointDisabledHover', "Disabled Breakpoint"), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointDisabledHover', "Disabled Breakpoint")), stickiness }; private static BREAKPOINT_UNVERIFIED_DECORATION: IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-unverified-glyph', - glyphMarginHoverMessage: nls.localize('breakpointUnverifieddHover', "Unverified Breakpoint"), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointUnverifieddHover', "Unverified Breakpoint")), stickiness }; private static BREAKPOINT_DIRTY_DECORATION: IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-unverified-glyph', - glyphMarginHoverMessage: nls.localize('breakpointDirtydHover', "Unverified breakpoint. File is modified, please restart debug session."), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointDirtydHover', "Unverified breakpoint. File is modified, please restart debug session.")), stickiness }; private static BREAKPOINT_UNSUPPORTED_DECORATION: IModelDecorationOptions = { glyphMarginClassName: 'debug-breakpoint-unsupported-glyph', - glyphMarginHoverMessage: nls.localize('breakpointUnsupported', "Conditional breakpoints not supported by this debug type"), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointUnsupported', "Conditional breakpoints not supported by this debug type")), stickiness }; diff --git a/src/vs/workbench/parts/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/parts/preferences/browser/keybindingsEditorContribution.ts index 8f1daf2c030..6bc0e824977 100644 --- a/src/vs/workbench/parts/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/parts/preferences/browser/keybindingsEditorContribution.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { MarkedString } from 'vs/base/common/htmlContent'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode, KeyMod, KeyChord, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -289,21 +289,21 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { } private _createDecoration(isError: boolean, uiLabel: string, usLabel: string, model: editorCommon.IModel, keyNode: Node): editorCommon.IModelDeltaDecoration { - let msg: MarkedString[]; + let msg: MarkdownString; let className: string; let beforeContentClassName: string; let overviewRulerColor: string; if (isError) { // this is the error case - msg = [NLS_KB_LAYOUT_ERROR_MESSAGE]; + msg = new MarkdownString().appendText(NLS_KB_LAYOUT_ERROR_MESSAGE); className = 'keybindingError'; beforeContentClassName = 'inlineKeybindingError'; overviewRulerColor = 'rgba(250, 100, 100, 0.6)'; } else { // this is the info case if (usLabel && uiLabel !== usLabel) { - msg = [ + msg = new MarkdownString( nls.localize({ key: 'defineKeybinding.kbLayoutLocalAndUSMessage', comment: [ @@ -311,9 +311,9 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { 'The placeholders will contain a keyboard combination e.g. Ctrl+Shift+/' ] }, "**{0}** for your current keyboard layout (**{1}** for US standard).", uiLabel, usLabel) - ]; + ); } else { - msg = [ + msg = new MarkdownString( nls.localize({ key: 'defineKeybinding.kbLayoutLocalMessage', comment: [ @@ -321,7 +321,7 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { 'The placeholder will contain a keyboard combination e.g. Ctrl+Shift+/' ] }, "**{0}** for your current keyboard layout.", uiLabel) - ]; + ); } className = 'keybindingInfo'; beforeContentClassName = 'inlineKeybindingInfo'; diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 085a79ddb96..4dc8cb2614b 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -32,6 +32,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { MarkdownString } from 'vs/base/common/htmlContent'; export interface IPreferencesRenderer extends IDisposable { preferencesModel: IPreferencesEditorModel; @@ -1022,7 +1023,7 @@ class UnsupportedWorkbenchSettingsRenderer extends Disposable { stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, inlineClassName: 'dim-configuration', beforeContentClassName: 'unsupportedWorkbenhSettingInfo', - hoverMessage: nls.localize('unsupportedWorkbenchSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.") + hoverMessage: new MarkdownString().appendText(nls.localize('unsupportedWorkbenchSetting', "This setting cannot be applied now. It will be applied when you open this folder directly.")) }); private createDecoration(range: IRange, model: editorCommon.IModel): editorCommon.IModelDeltaDecoration { @@ -1097,4 +1098,4 @@ class WorkspaceConfigurationRenderer extends Disposable { } super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index cd2ce8fa139..74c8fb9be87 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -34,6 +34,7 @@ import { Color } from 'vs/base/common/color'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { MarkdownString } from 'vs/base/common/htmlContent'; export class SettingsHeaderWidget extends Widget implements IViewZone { @@ -621,7 +622,7 @@ export class EditPreferenceWidget extends Disposable { newDecoration.push({ options: { glyphMarginClassName: EditPreferenceWidget.GLYPH_MARGIN_CLASS_NAME, - glyphMarginHoverMessage: hoverMessage, + glyphMarginHoverMessage: new MarkdownString().appendText(hoverMessage), stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }, range: { @@ -646,4 +647,4 @@ export class EditPreferenceWidget extends Disposable { this.hide(); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 03afe2edff7..eda19b59181 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -446,8 +446,8 @@ suite('ExtHostLanguageFeatures', function () { return getHover(model, new EditorPosition(1, 1)).then(value => { assert.equal(value.length, 2); let [first, second] = value as Hover[]; - assert.equal(first.contents[0], 'registered second'); - assert.equal(second.contents[0], 'registered first'); + assert.equal(first.contents[0].value, 'registered second'); + assert.equal(second.contents[0].value, 'registered first'); }); }); });