diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index d4ca7479c7f..16f632117f0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -10,7 +10,6 @@ import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -31,6 +30,8 @@ import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelServiceImpl'; +const $ = dom.$; + class InspectEditorTokensController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.inspectEditorTokens'; @@ -151,23 +152,11 @@ function renderTokenText(tokenText: string): string { let charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: - result += '→'; + result += '\u2192'; // → break; case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; + result += '\u00B7'; // · break; default: @@ -246,8 +235,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (this._isDisposed) { return; } - let text = this._compute(grammar, semanticTokens, position); - this._domNode.innerHTML = text; + this._compute(grammar, semanticTokens, position); this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; this._editor.layoutContentWidget(this); }, (err) => { @@ -268,11 +256,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return this._themeService.getColorTheme().semanticHighlighting; } - private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position): string { + private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position) { const textMateTokenInfo = grammar && this._getTokensAtPosition(grammar, position); const semanticTokenInfo = semanticTokens && this._getSemanticTokenAtPosition(semanticTokens, position); if (!textMateTokenInfo && !semanticTokenInfo) { - return 'No grammar or semantic tokens available.'; + dom.reset(this._domNode, 'No grammar or semantic tokens available.'); + return; } let tmMetadata = textMateTokenInfo?.metadata; @@ -283,91 +272,125 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const tokenText = semTokenText || tmTokenText || ''; - let result = ''; - result += `

${tokenText}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

`; - result += `
`; - - result += ``; - result += ``; - result += ``; - - result += this._formatMetadata(semMetadata, tmMetadata); - result += `
`; + dom.reset(this._domNode, + $('h2.tiw-token', undefined, + tokenText, + $('span.tiw-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); + dom.append(this._domNode, $('hr.tiw-metadata-separator', { 'style': 'clear:both' })); + dom.append(this._domNode, $('table.tiw-metadata-table', undefined, + $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'language'), + $('td.tiw-metadata-value', undefined, tmMetadata?.languageIdentifier.language || '') + ), + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'standard token type' as string), + $('td.tiw-metadata-value', undefined, this._tokenTypeToString(tmMetadata?.tokenType || StandardTokenType.Other)) + ), + ...this._formatMetadata(semMetadata, tmMetadata) + ) + )); if (semanticTokenInfo) { - result += `
`; - result += ``; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table', undefined)); + const tbody = dom.append(table, $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'semantic token type' as string), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.type) + ) + )); if (semanticTokenInfo.modifiers.length) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'modifiers'), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.modifiers.join(' ')), + )); } if (semanticTokenInfo.metadata) { const properties: (keyof TokenStyleData)[] = ['foreground', 'bold', 'italic', 'underline']; const propertiesByDefValue: { [rule: string]: string[] } = {}; - const allDefValues = []; // remember the order + const allDefValues = new Array<[Array, string]>(); // remember the order // first collect to detect when the same rule is used for multiple properties for (let property of properties) { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); - let properties = propertiesByDefValue[defValue]; + const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join(); + let properties = propertiesByDefValue[defValueStr]; if (!properties) { - propertiesByDefValue[defValue] = properties = []; - allDefValues.push(defValue); + propertiesByDefValue[defValueStr] = properties = []; + allDefValues.push([defValue, defValueStr]); } properties.push(property); } } - for (let defValue of allDefValues) { - result += ``; + for (const [defValue, defValueStr] of allDefValues) { + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, propertiesByDefValue[defValueStr].join(', ')), + $('td.tiw-metadata-value', undefined, ...defValue) + )); } } - result += `
`; } if (textMateTokenInfo) { let theme = this._themeService.getColorTheme(); - result += `
`; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table')); + const tbody = dom.append(table, $('tbody')); + if (tmTokenText && tmTokenText !== tokenText) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate token' as string), + $('td.tiw-metadata-value', undefined, `${tmTokenText} (${tmTokenText.length})`) + )); } - let scopes = ''; + const scopes = new Array(); for (let i = textMateTokenInfo.token.scopes.length - 1; i >= 0; i--) { - scopes += escape(textMateTokenInfo.token.scopes[i]); + scopes.push(textMateTokenInfo.token.scopes[i]); if (i > 0) { - scopes += '
'; + scopes.push($('br')); } } - result += `
`; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate scopes' as string), + $('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes), + )); let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); const semForeground = semanticTokenInfo?.metadata?.foreground; if (matchingRule) { - let defValue = `${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; if (semForeground !== textMateTokenInfo.metadata.foreground) { + let defValue = $('code.tiw-theme-selector', undefined, + matchingRule.rawSelector, $('br'), JSON.stringify(matchingRule.settings, null, '\t')); if (semForeground) { - defValue = `${defValue}`; + defValue = $('s', undefined, defValue); } - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, defValue), + )); } } else if (!semForeground) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, 'No theme selector' as string), + )); } - result += `
`; } - return result; } - private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata) { - let result = ''; + private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata): Array { + const elements = new Array(); function render(property: 'foreground' | 'background') { let value = semantic?.[property] || tm?.[property]; if (value !== undefined) { const semanticStyle = semantic?.[property] ? 'tiw-metadata-semantic' : ''; - result += `${property}${value}`; - + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, property), + $(`td.tiw-metadata-value.${semanticStyle}`, undefined, value) + )); } return value; } @@ -377,17 +400,23 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (foreground && background) { const backgroundColor = Color.fromHex(background), foregroundColor = Color.fromHex(foreground); if (backgroundColor.isOpaque()) { - result += `contrast ratio${backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'contrast ratio' as string), + $('td.tiw-metadata-value', undefined, backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)) + )); } else { - result += 'Contrast ratio cannot be precise for background colors that use transparency'; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'Contrast ratio cannot be precise for background colors that use transparency' as string), + $('td.tiw-metadata-value') + )); } } - let fontStyleLabels: string[] = []; + const fontStyleLabels = new Array(); function addStyle(key: 'bold' | 'italic' | 'underline') { if (semantic && semantic[key]) { - fontStyleLabels.push(``); + fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key)); } else if (tm && tm[key]) { fontStyleLabels.push(key); } @@ -396,9 +425,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { addStyle('italic'); addStyle('underline'); if (fontStyleLabels.length) { - result += `font style${fontStyleLabels.join(' ')}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'font style' as string), + $('td.tiw-metadata-value', undefined, fontStyleLabels.join(' ')) + )); } - return result; + return elements; } private _decodeMetadata(metadata: number): IDecodedMetadata { @@ -549,9 +581,10 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return null; } - private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): string { + private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): Array { + const elements = new Array(); if (definition === undefined) { - return ''; + return elements; } const theme = this._themeService.getColorTheme() as ColorThemeData; @@ -561,20 +594,27 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); - return `${escape(scopesDefinition.scope.join(' '))}
${strScopes}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; + elements.push( + scopesDefinition.scope.join(' '), + $('br'), + $('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t'))); + return elements; } - return ''; + return elements; } else if (SemanticTokenRule.is(definition)) { const scope = theme.getTokenStylingRuleScope(definition); if (scope === 'setting') { - return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } else if (scope === 'theme') { - return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } - return ''; + return elements; } else { const style = theme.resolveTokenStyleValue(definition); - return `Default: ${style ? this._renderStyleProperty(style, property) : ''}`; + elements.push(`Default: ${style ? this._renderStyleProperty(style, property) : ''}`); + return elements; } }