From 0f042f32663bfae8e63222345abd0d91a8188121 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 17 Nov 2021 14:34:46 +0000 Subject: [PATCH] Render markdown for preferences --- packages/core/package.json | 1 + packages/core/src/browser/core-preferences.ts | 5 +- packages/core/src/browser/index.ts | 1 + .../src/browser/markdown-renderer.spec.ts | 78 +++++++++++++++ .../core/src/browser/markdown-renderer.ts | 76 ++++++++++++++ .../editor/src/browser/editor-preferences.ts | 12 +-- .../src/browser/filesystem-preferences.ts | 4 +- .../src/browser/hosted-plugin-preferences.ts | 2 +- .../src/browser/preference-tree-model.ts | 5 + .../preferences/src/browser/style/index.css | 5 + .../browser/util/preference-tree-generator.ts | 6 ++ .../preference-tree-label-provider.spec.ts | 1 + .../src/browser/util/preference-types.ts | 1 + .../views/components/preference-json-input.ts | 4 +- .../components/preference-node-renderer.ts | 99 +++++++++++++++++-- .../views/preference-searchbar-widget.tsx | 18 ++-- .../search-in-workspace-preferences.ts | 2 +- .../src/browser/terminal-preferences.ts | 4 +- 18 files changed, 289 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/browser/markdown-renderer.spec.ts create mode 100644 packages/core/src/browser/markdown-renderer.ts diff --git a/packages/core/package.json b/packages/core/package.json index 9935c94446ce8..fd9946cfe0314 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,6 +50,7 @@ "keytar": "7.2.0", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", + "markdown-it": "^8.4.0", "nsfw": "^2.1.2", "p-debounce": "^2.1.0", "perfect-scrollbar": "^1.3.0", diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 35cec878581d6..e2a31831828cf 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -58,8 +58,7 @@ export const corePreferenceSchema: PreferenceSchema = { 'keyCode', ], default: 'code', - description: nls.localizeByDefault( - 'Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.') + markdownDescription: nls.localizeByDefault('Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.') }, 'window.menuBarVisibility': { type: 'string', @@ -89,7 +88,7 @@ export const corePreferenceSchema: PreferenceSchema = { 'workbench.editor.highlightModifiedTabs': { 'type': 'boolean', // eslint-disable-next-line max-len - 'description': nls.localizeByDefault('Controls whether a top border is drawn on modified (dirty) editor tabs or not. This value is ignored when `#workbench.editor.showTabs#` is disabled.'), + 'markdownDescription': nls.localize('theia/core/highlightModifiedTabs', 'Controls whether a top border is drawn on modified (dirty) editor tabs or not.'), 'default': false }, 'workbench.editor.closeOnFileDelete': { diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts index caa4f76d459b6..2553385e444a1 100644 --- a/packages/core/src/browser/index.ts +++ b/packages/core/src/browser/index.ts @@ -41,3 +41,4 @@ export * from './core-preferences'; export * from './view-container'; export * from './breadcrumbs'; export * from './tooltip-service'; +export * from './markdown-renderer'; diff --git a/packages/core/src/browser/markdown-renderer.spec.ts b/packages/core/src/browser/markdown-renderer.spec.ts new file mode 100644 index 0000000000000..0cab6d704956f --- /dev/null +++ b/packages/core/src/browser/markdown-renderer.spec.ts @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (C) 2021 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { enableJSDOM } from '../browser/test/jsdom'; +let disableJSDOM = enableJSDOM(); + +import { expect } from 'chai'; +import * as markdownit from 'markdown-it'; +import { MarkdownRenderer } from './markdown-renderer'; + +disableJSDOM(); + +describe('MarkdownRenderer', () => { + + before(() => disableJSDOM = enableJSDOM()); + after(() => disableJSDOM()); + + it('Should render markdown', () => { + const markdownRenderer = new MarkdownRenderer(); + const result = markdownRenderer.renderInline('[title](link)').innerHTML; + expect(result).to.be.equal('title'); + }); + + it('Should accept and use custom engine', () => { + const engine = markdownit(); + const originalTextRenderer = engine.renderer.rules.text!; + engine.renderer.rules.text = (tokens, idx, options, env, self) => `[${originalTextRenderer(tokens, idx, options, env, self)}]`; + const markdownRenderer = new MarkdownRenderer(engine); + const result = markdownRenderer.renderInline('text').innerHTML; + expect(result).to.be.equal('[text]'); + }); + + it('Should modify rendered markdown in place', () => { + const markdownRenderer = new MarkdownRenderer().modify('a', a => { + a.href = 'something-else'; + }); + const result = markdownRenderer.renderInline('[title](link)').innerHTML; + expect(result).to.be.equal('title'); + }); + + it('Should modify descendants of children', () => { + const markdownRenderer = new MarkdownRenderer().modify('em', em => { + const strong = document.createElement('strong'); + // eslint-disable-next-line no-unsanitized/property + strong.innerHTML = em.innerHTML; + return strong; + }); + const result = markdownRenderer.render('**bold *bold and italic***').innerHTML; + expect(result).to.be.equal('

bold bold and italic

\n'); + }); + + it('Should modify descendants of children after previous modification', () => { + const markdownRenderer = new MarkdownRenderer().modify('em', em => { + const strong = document.createElement('strong'); + // eslint-disable-next-line no-unsanitized/property + strong.innerHTML = em.innerHTML; + return strong; + }).modify('strong', strong => { // Will pick up both the original and modified strong element + const textNode = strong.childNodes.item(0); + textNode.textContent = `changed_${textNode.textContent}`; + }); + const result = markdownRenderer.render('**bold *bold and italic***').innerHTML; + expect(result).to.be.equal('

changed_bold changed_bold and italic

\n'); + }); +}); diff --git a/packages/core/src/browser/markdown-renderer.ts b/packages/core/src/browser/markdown-renderer.ts new file mode 100644 index 0000000000000..89695dfaa22e6 --- /dev/null +++ b/packages/core/src/browser/markdown-renderer.ts @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (C) 2021 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as DOMPurify from 'dompurify'; +import * as markdownit from 'markdown-it'; + +export class MarkdownRenderer { + + protected engine: markdownit; + protected callbacks = new Map Element | void)[]>(); + + constructor(engine?: markdownit) { + this.engine = engine ?? markdownit(); + } + + /** + * Adds a modification callback that is applied to every element with the specified tag after rendering to HTML. + * + * @param tag The tag that this modification applies to. + * @param callback The modification to apply on every selected rendered element. Can either modify the element in place or return a new element. + */ + modify(tag: K, callback: (element: HTMLElementTagNameMap[K]) => Element | void): MarkdownRenderer { + if (this.callbacks.has(tag)) { + this.callbacks.get(tag)!.push(callback); + } else { + this.callbacks.set(tag, [callback]); + } + return this; + } + + render(markdown: string): HTMLElement { + return this.renderInternal(this.engine.render(markdown)); + } + + renderInline(markdown: string): HTMLElement { + return this.renderInternal(this.engine.renderInline(markdown)); + } + + protected renderInternal(renderedHtml: string): HTMLElement { + const div = this.sanitizeHtml(renderedHtml); + for (const [tag, calls] of this.callbacks) { + for (const callback of calls) { + const elements = Array.from(div.getElementsByTagName(tag)); + for (const element of elements) { + const result = callback(element); + if (result) { + const parent = element.parentElement; + if (parent) { + parent.replaceChild(result, element); + } + } + } + } + } + return div; + } + + protected sanitizeHtml(html: string): HTMLElement { + const div = document.createElement('div'); + div.innerHTML = DOMPurify.sanitize(html); + return div; + } +} diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index 2507e23e3ef98..6626c4bb51de8 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -122,7 +122,7 @@ const codeEditorPreferenceProperties = { }, 'editor.semanticHighlighting.enabled': { 'enum': [true, false, 'configuredByTheme'], - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localizeByDefault('Semantic highlighting enabled for all color themes.'), nls.localizeByDefault('Semantic highlighting disabled for all color themes.'), nls.localizeByDefault('Semantic highlighting is configured by the current color theme\'s `semanticHighlighting` setting.') @@ -327,7 +327,7 @@ const codeEditorPreferenceProperties = { 'default': 0, 'minimum': 0, 'maximum': 100, - 'description': nls.localizeByDefault('Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.') + 'markdownDescription': nls.localizeByDefault('Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.') }, 'editor.colorDecorators': { 'description': nls.localizeByDefault('Controls whether the editor should render the inline color decorators and color picker.'), @@ -392,11 +392,11 @@ const codeEditorPreferenceProperties = { 'maximum': 1073741824 }, 'editor.cursorSurroundingLinesStyle': { - 'enumDescriptions': [ + 'markdownEnumDescriptions': [ nls.localizeByDefault('`cursorSurroundingLines` is enforced only when triggered via the keyboard or API.'), nls.localizeByDefault('`cursorSurroundingLines` is enforced always.') ], - 'description': nls.localizeByDefault('Controls when `cursorSurroundingLines` should be enforced.'), + 'markdownDescription': nls.localizeByDefault('Controls when `cursorSurroundingLines` should be enforced.'), 'type': 'string', 'enum': [ 'default', @@ -1111,7 +1111,7 @@ const codeEditorPreferenceProperties = { 'editor.inlineHints.fontSize': { 'type': 'number', 'default': EDITOR_FONT_DEFAULTS.fontSize, - description: nls.localizeByDefault('Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.') + markdownDescription: nls.localizeByDefault('Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.') }, 'editor.inlineHints.fontFamily': { 'type': 'string', @@ -1173,7 +1173,7 @@ const codeEditorPreferenceProperties = { 'editor.suggest.insertHighlight': { 'type': 'boolean', 'default': false, - 'description': nls.localize('theia/editor/suggest.insertHighlight', 'Controls whether unexpected text modifications while accepting completions should be highlighted, e.g `insertMode` is `replace` but the completion only supports `insert`.') + 'markdownDescription': nls.localize('theia/editor/suggest.insertHighlight', 'Controls whether unexpected text modifications while accepting completions should be highlighted, e.g `insertMode` is `replace` but the completion only supports `insert`.') }, 'editor.suggest.filterGraceful': { 'type': 'boolean', diff --git a/packages/filesystem/src/browser/filesystem-preferences.ts b/packages/filesystem/src/browser/filesystem-preferences.ts index 434ab56d5fd4d..9b99b5c3443c2 100644 --- a/packages/filesystem/src/browser/filesystem-preferences.ts +++ b/packages/filesystem/src/browser/filesystem-preferences.ts @@ -55,7 +55,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = { type: 'object', default: { '**/.git': true, '**/.svn': true, '**/.hg': true, '**/CVS': true, '**/.DS_Store': true }, // eslint-disable-next-line max-len - description: nls.localize('theia/filesystem/filesExclude', 'Configure glob patterns for excluding files and folders. For example, the file Explorer decides which files and folders to show or hide based on this setting. Refer to the `#search.exclude#` setting to define search specific excludes.'), + markdownDescription: nls.localize('theia/filesystem/filesExclude', 'Configure glob patterns for excluding files and folders. For example, the file Explorer decides which files and folders to show or hide based on this setting.'), scope: 'resource' }, 'files.enableTrash': { @@ -65,7 +65,7 @@ export const filesystemPreferenceSchema: PreferenceSchema = { }, 'files.associations': { type: 'object', - description: nls.localizeByDefault( + markdownDescription: nls.localizeByDefault( 'Configure file associations to languages (e.g. `\"*.extension\": \"html\"`). These have precedence over the default associations of the languages installed.' ) }, diff --git a/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts b/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts index 8372769c54ff1..7ca3503be6f3d 100644 --- a/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts +++ b/packages/plugin-dev/src/browser/hosted-plugin-preferences.ts @@ -37,7 +37,7 @@ export const HostedPluginConfigSchema: PreferenceSchema = { items: { type: 'string' }, - description: nls.localize( + markdownDescription: nls.localize( 'theia/plugin-dev/launchOutFiles', 'Array of glob patterns for locating generated JavaScript files (`${pluginPath}` will be replaced by plugin actual path).' ), diff --git a/packages/preferences/src/browser/preference-tree-model.ts b/packages/preferences/src/browser/preference-tree-model.ts index b05f81cb4105b..16dc5f0a38415 100644 --- a/packages/preferences/src/browser/preference-tree-model.ts +++ b/packages/preferences/src/browser/preference-tree-model.ts @@ -227,6 +227,11 @@ export class PreferenceTreeModel extends TreeModelImpl { } } + getNodeFromPreferenceId(id: string): Preference.TreeNode | undefined { + const node = this.getNode(this.treeGenerator.getNodeId(id)); + return node && Preference.TreeNode.is(node) ? node : undefined; + } + /** * @returns true if selection changed, false otherwise */ diff --git a/packages/preferences/src/browser/style/index.css b/packages/preferences/src/browser/style/index.css index f57c7692083e4..90aa949ad3624 100644 --- a/packages/preferences/src/browser/style/index.css +++ b/packages/preferences/src/browser/style/index.css @@ -301,6 +301,11 @@ line-height: 18px; } +.theia-settings-container .pref-description a { + text-decoration-line: none; + cursor: pointer; +} + .theia-settings-container .theia-select:focus { outline-width: 1px; outline-style: solid; diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts index 8c67a0569df28..06c2f87418750 100644 --- a/packages/preferences/src/browser/util/preference-tree-generator.ts +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -126,6 +126,12 @@ export class PreferenceTreeGenerator { return root; }; + getNodeId(preferenceId: string): string { + const expectedGroup = this.getGroupName(preferenceId.split('.')); + const expectedId = `${expectedGroup}@${preferenceId}`; + return expectedId; + } + protected getGroupName(labels: string[]): string { const defaultGroup = labels[0]; if (this.topLevelCategories.has(defaultGroup)) { diff --git a/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts b/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts index 19b208e285a07..d4f9fed57174c 100644 --- a/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts +++ b/packages/preferences/src/browser/util/preference-tree-label-provider.spec.ts @@ -100,6 +100,7 @@ describe('preference-tree-label-provider', () => { visible: true, selected: false, depth: 2, + preferenceId: property, preference: { data: {} } }; diff --git a/packages/preferences/src/browser/util/preference-types.ts b/packages/preferences/src/browser/util/preference-types.ts index 68af26a000bff..8b701b5de67f6 100644 --- a/packages/preferences/src/browser/util/preference-types.ts +++ b/packages/preferences/src/browser/util/preference-types.ts @@ -64,6 +64,7 @@ export namespace Preference { export interface LeafNode extends BaseTreeNode { depth: number; preference: { data: PreferenceDataProperty }; + preferenceId: string; } export namespace LeafNode { diff --git a/packages/preferences/src/browser/views/components/preference-json-input.ts b/packages/preferences/src/browser/views/components/preference-json-input.ts index b98e5651716c8..72eb0675bce1f 100644 --- a/packages/preferences/src/browser/views/components/preference-json-input.ts +++ b/packages/preferences/src/browser/views/components/preference-json-input.ts @@ -16,7 +16,7 @@ import { PreferenceLeafNodeRenderer } from './preference-node-renderer'; import { injectable, inject } from '@theia/core/shared/inversify'; -import { CommandService } from '@theia/core/lib/common'; +import { CommandService, nls } from '@theia/core/lib/common'; import { PreferencesCommands } from '../../util/preference-types'; import { JSONValue } from '@theia/core/shared/@phosphor/coreutils'; @@ -25,7 +25,7 @@ export class PreferenceJSONLinkRenderer extends PreferenceLeafNodeRenderer PreferenceNodeRenderer; @@ -151,17 +155,20 @@ export abstract class PreferenceLeafNodeRenderer | undefined; protected isModifiedFromDefault = false; + protected markdownRenderer: MarkdownRenderer; @postConstruct() protected init(): void { this.setId(); this.updateInspection(); + this.markdownRenderer = this.buildMarkdownRenderer(); this.domNode = this.createDomNode(); this.updateModificationStatus(); } @@ -170,6 +177,78 @@ export abstract class PreferenceLeafNodeRenderer(this.id, this.scopeTracker.currentScope.uri); } + protected buildMarkdownRenderer(): MarkdownRenderer { + return new MarkdownRenderer() + .modify('a', link => { + const href = link.href; + link.href = '#'; + link.title = href; + this.setClickListener(link, e => this.openLink(e, href)); + }) + .modify('code', code => { + const innerText = code.innerText; + // Linked preferences always start and end with `#` + if (innerText.startsWith('#') && innerText.endsWith('#')) { + const preferenceId = innerText.substring(1, innerText.length - 1); + const preferenceNode = this.model.getNodeFromPreferenceId(preferenceId); + if (preferenceNode) { + let name = this.labelProvider.getName(preferenceNode); + const prefix = this.labelProvider.getPrefix(preferenceNode, true); + if (prefix) { + name = prefix + name; + } + const link = document.createElement('a'); + this.setClickListener(link, e => this.selectPreference(e, preferenceId)); + link.innerText = name; + link.title = `#${preferenceId}`; + link.href = '#'; + return link; + } else { + console.warn(`Linked preference "${preferenceId}" not found. Source: "${this.preferenceNode.preferenceId}"`); + } + } + return code; + }); + } + + protected setClickListener(element: HTMLElement, callback: (event: MouseEvent) => void): void { + // onclick only handles left button clicks + // onauxclick handles all other cases + element.onclick = callback; + element.onauxclick = callback; + // Disables showing a context menu when right clicking + element.oncontextmenu = () => false; + } + + protected openLink(event: MouseEvent, href: string): void { + event.preventDefault(); + event.stopPropagation(); + // Exclude right click + if (event.button < 2) { + // Opens link in external browser + this.windowService.openNewWindow(href, { external: true }); + } + } + + protected async selectPreference(event: MouseEvent, preferenceId: string): Promise { + event.preventDefault(); + event.stopPropagation(); + // Exclude right click + if (event.button < 2) { + // Selects the rendered html preference node that does not belong to the commonly used group + const selector = `li[data-pref-id="${preferenceId}"]:not([data-node-id^="commonly-used@"])`; + const element = document.querySelector(selector); + if (element) { + if (element.classList.contains('hidden')) { + // We clear the search term as we have clicked on a hidden preference + await this.searchbar.updateSearchTerm(''); + await animationFrame(); + } + element.scrollIntoView(); + } + } + } + protected createDomNode(): HTMLLIElement { const wrapper = document.createElement('li'); wrapper.classList.add('single-pref'); @@ -191,12 +270,12 @@ export abstract class PreferenceLeafNodeRenderer): void => { - this.search(e.target.value); - }; + protected handleSearch = (e: React.ChangeEvent): Promise => this.search(e.target.value); - protected search = debounce((value: string) => { + protected search = debounce(async (value: string) => { this.onFilterStringChangedEmitter.fire(value); this.update(); }, 200); @@ -64,11 +62,11 @@ export class PreferencesSearchbarWidget extends ReactWidget implements StatefulW * Clears the search input and all search results. * @param e on-click mouse event. */ - protected clearSearchResults = (e: React.MouseEvent): void => { + protected clearSearchResults = async (e: React.MouseEvent): Promise => { const search = document.getElementById(PreferencesSearchbarWidget.SEARCHBAR_ID) as HTMLInputElement; if (search) { search.value = ''; - this.search(search.value); + await this.search(search.value); this.update(); } }; @@ -127,13 +125,13 @@ export class PreferencesSearchbarWidget extends ReactWidget implements StatefulW return search?.value; } - updateSearchTerm(searchTerm: string): void { + async updateSearchTerm(searchTerm: string): Promise { const search = document.getElementById(PreferencesSearchbarWidget.SEARCHBAR_ID) as HTMLInputElement; - if (!search) { + if (!search || search.value === searchTerm) { return; } search.value = searchTerm; - this.search(search.value); + await this.search(search.value); this.update(); } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts b/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts index 0b65c5615290f..2ff9bc192b81a 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-preferences.ts @@ -39,7 +39,7 @@ export const searchInWorkspacePreferencesSchema: PreferenceSchema = { }, 'search.searchOnTypeDebouncePeriod': { // eslint-disable-next-line max-len - description: nls.localizeByDefault('When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.'), + markdownDescription: nls.localizeByDefault('When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.'), default: 300, type: 'number', }, diff --git a/packages/terminal/src/browser/terminal-preferences.ts b/packages/terminal/src/browser/terminal-preferences.ts index 6b02b73e1f316..e21438909d029 100644 --- a/packages/terminal/src/browser/terminal-preferences.ts +++ b/packages/terminal/src/browser/terminal-preferences.ts @@ -36,7 +36,7 @@ export const TerminalConfigSchema: PreferenceSchema = { }, 'terminal.integrated.fontFamily': { type: 'string', - description: nls.localizeByDefault("Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."), + markdownDescription: nls.localizeByDefault("Controls the font family of the terminal, this defaults to `#editor.fontFamily#`'s value."), default: EDITOR_FONT_DEFAULTS.fontFamily }, 'terminal.integrated.fontSize': { @@ -79,7 +79,7 @@ export const TerminalConfigSchema: PreferenceSchema = { default: 1000 }, 'terminal.integrated.fastScrollSensitivity': { - description: nls.localizeByDefault('Scrolling speed multiplier when pressing `Alt`.'), + markdownDescription: nls.localizeByDefault('Scrolling speed multiplier when pressing `Alt`.'), type: 'number', default: 5, },