Path: blob/main/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './inspectTokens.css';6import { $, append, reset } from '../../../../base/browser/dom.js';7import { CharCode } from '../../../../base/common/charCode.js';8import { Color } from '../../../../base/common/color.js';9import { KeyCode } from '../../../../base/common/keyCodes.js';10import { Disposable } from '../../../../base/common/lifecycle.js';11import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';12import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution, EditorContributionInstantiation } from '../../../browser/editorExtensions.js';13import { Position } from '../../../common/core/position.js';14import { IEditorContribution } from '../../../common/editorCommon.js';15import { ITextModel } from '../../../common/model.js';16import { IState, ITokenizationSupport, TokenizationRegistry, ILanguageIdCodec, Token } from '../../../common/languages.js';17import { FontStyle, StandardTokenType, TokenMetadata } from '../../../common/encodedTokenAttributes.js';18import { NullState, nullTokenize, nullTokenizeEncoded } from '../../../common/languages/nullTokenize.js';19import { ILanguageService } from '../../../common/languages/language.js';20import { IStandaloneThemeService } from '../../common/standaloneTheme.js';21import { InspectTokensNLS } from '../../../common/standaloneStrings.js';222324class InspectTokensController extends Disposable implements IEditorContribution {2526public static readonly ID = 'editor.contrib.inspectTokens';2728public static get(editor: ICodeEditor): InspectTokensController | null {29return editor.getContribution<InspectTokensController>(InspectTokensController.ID);30}3132private readonly _editor: ICodeEditor;33private readonly _languageService: ILanguageService;34private _widget: InspectTokensWidget | null;3536constructor(37editor: ICodeEditor,38@IStandaloneThemeService standaloneColorService: IStandaloneThemeService,39@ILanguageService languageService: ILanguageService40) {41super();42this._editor = editor;43this._languageService = languageService;44this._widget = null;4546this._register(this._editor.onDidChangeModel((e) => this.stop()));47this._register(this._editor.onDidChangeModelLanguage((e) => this.stop()));48this._register(TokenizationRegistry.onDidChange((e) => this.stop()));49this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop()));50}5152public override dispose(): void {53this.stop();54super.dispose();55}5657public launch(): void {58if (this._widget) {59return;60}61if (!this._editor.hasModel()) {62return;63}64this._widget = new InspectTokensWidget(this._editor, this._languageService);65}6667public stop(): void {68if (this._widget) {69this._widget.dispose();70this._widget = null;71}72}73}7475class InspectTokens extends EditorAction {7677constructor() {78super({79id: 'editor.action.inspectTokens',80label: InspectTokensNLS.inspectTokensAction,81alias: 'Developer: Inspect Tokens',82precondition: undefined83});84}8586public run(accessor: ServicesAccessor, editor: ICodeEditor): void {87const controller = InspectTokensController.get(editor);88controller?.launch();89}90}9192interface ICompleteLineTokenization {93startState: IState;94tokens1: Token[];95tokens2: Uint32Array;96endState: IState;97}9899interface IDecodedMetadata {100languageId: string;101tokenType: StandardTokenType;102fontStyle: FontStyle;103foreground: Color;104background: Color;105}106107function renderTokenText(tokenText: string): string {108let result: string = '';109for (let charIndex = 0, len = tokenText.length; charIndex < len; charIndex++) {110const charCode = tokenText.charCodeAt(charIndex);111switch (charCode) {112case CharCode.Tab:113result += '\u2192'; // →114break;115116case CharCode.Space:117result += '\u00B7'; // ·118break;119120default:121result += String.fromCharCode(charCode);122}123}124return result;125}126127function getSafeTokenizationSupport(languageIdCodec: ILanguageIdCodec, languageId: string): ITokenizationSupport {128const tokenizationSupport = TokenizationRegistry.get(languageId);129if (tokenizationSupport) {130return tokenizationSupport;131}132const encodedLanguageId = languageIdCodec.encodeLanguageId(languageId);133return {134getInitialState: () => NullState,135tokenize: (line: string, hasEOL: boolean, state: IState) => nullTokenize(languageId, state),136tokenizeEncoded: (line: string, hasEOL: boolean, state: IState) => nullTokenizeEncoded(encodedLanguageId, state)137};138}139140class InspectTokensWidget extends Disposable implements IContentWidget {141142private static readonly _ID = 'editor.contrib.inspectTokensWidget';143144// Editor.IContentWidget.allowEditorOverflow145public allowEditorOverflow = true;146147private readonly _editor: IActiveCodeEditor;148private readonly _languageService: ILanguageService;149private readonly _tokenizationSupport: ITokenizationSupport;150private readonly _model: ITextModel;151private readonly _domNode: HTMLElement;152153constructor(154editor: IActiveCodeEditor,155languageService: ILanguageService156) {157super();158this._editor = editor;159this._languageService = languageService;160this._model = this._editor.getModel();161this._domNode = document.createElement('div');162this._domNode.className = 'tokens-inspect-widget';163this._tokenizationSupport = getSafeTokenizationSupport(this._languageService.languageIdCodec, this._model.getLanguageId());164this._compute(this._editor.getPosition());165this._register(this._editor.onDidChangeCursorPosition((e) => this._compute(this._editor.getPosition())));166this._editor.addContentWidget(this);167}168169public override dispose(): void {170this._editor.removeContentWidget(this);171super.dispose();172}173174public getId(): string {175return InspectTokensWidget._ID;176}177178private _compute(position: Position): void {179const data = this._getTokensAtLine(position.lineNumber);180181let token1Index = 0;182for (let i = data.tokens1.length - 1; i >= 0; i--) {183const t = data.tokens1[i];184if (position.column - 1 >= t.offset) {185token1Index = i;186break;187}188}189190let token2Index = 0;191for (let i = (data.tokens2.length >>> 1); i >= 0; i--) {192if (position.column - 1 >= data.tokens2[(i << 1)]) {193token2Index = i;194break;195}196}197198const lineContent = this._model.getLineContent(position.lineNumber);199let tokenText = '';200if (token1Index < data.tokens1.length) {201const tokenStartIndex = data.tokens1[token1Index].offset;202const tokenEndIndex = token1Index + 1 < data.tokens1.length ? data.tokens1[token1Index + 1].offset : lineContent.length;203tokenText = lineContent.substring(tokenStartIndex, tokenEndIndex);204}205reset(this._domNode,206$('h2.tm-token', undefined, renderTokenText(tokenText),207$('span.tm-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`)));208209append(this._domNode, $('hr.tokens-inspect-separator', { 'style': 'clear:both' }));210211const metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null;212append(this._domNode, $('table.tm-metadata-table', undefined,213$('tbody', undefined,214$('tr', undefined,215$('td.tm-metadata-key', undefined, 'language'),216$('td.tm-metadata-value', undefined, `${metadata ? metadata.languageId : '-?-'}`)217),218$('tr', undefined,219$('td.tm-metadata-key', undefined, 'token type' as string),220$('td.tm-metadata-value', undefined, `${metadata ? this._tokenTypeToString(metadata.tokenType) : '-?-'}`)221),222$('tr', undefined,223$('td.tm-metadata-key', undefined, 'font style' as string),224$('td.tm-metadata-value', undefined, `${metadata ? this._fontStyleToString(metadata.fontStyle) : '-?-'}`)225),226$('tr', undefined,227$('td.tm-metadata-key', undefined, 'foreground'),228$('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.foreground) : '-?-'}`)229),230$('tr', undefined,231$('td.tm-metadata-key', undefined, 'background'),232$('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.background) : '-?-'}`)233)234)235));236append(this._domNode, $('hr.tokens-inspect-separator'));237238if (token1Index < data.tokens1.length) {239append(this._domNode, $('span.tm-token-type', undefined, data.tokens1[token1Index].type));240}241242this._editor.layoutContentWidget(this);243}244245private _decodeMetadata(metadata: number): IDecodedMetadata {246const colorMap = TokenizationRegistry.getColorMap()!;247const languageId = TokenMetadata.getLanguageId(metadata);248const tokenType = TokenMetadata.getTokenType(metadata);249const fontStyle = TokenMetadata.getFontStyle(metadata);250const foreground = TokenMetadata.getForeground(metadata);251const background = TokenMetadata.getBackground(metadata);252return {253languageId: this._languageService.languageIdCodec.decodeLanguageId(languageId),254tokenType: tokenType,255fontStyle: fontStyle,256foreground: colorMap[foreground],257background: colorMap[background]258};259}260261private _tokenTypeToString(tokenType: StandardTokenType): string {262switch (tokenType) {263case StandardTokenType.Other: return 'Other';264case StandardTokenType.Comment: return 'Comment';265case StandardTokenType.String: return 'String';266case StandardTokenType.RegEx: return 'RegEx';267default: return '??';268}269}270271private _fontStyleToString(fontStyle: FontStyle): string {272let r = '';273if (fontStyle & FontStyle.Italic) {274r += 'italic ';275}276if (fontStyle & FontStyle.Bold) {277r += 'bold ';278}279if (fontStyle & FontStyle.Underline) {280r += 'underline ';281}282if (fontStyle & FontStyle.Strikethrough) {283r += 'strikethrough ';284}285if (r.length === 0) {286r = '---';287}288return r;289}290291private _getTokensAtLine(lineNumber: number): ICompleteLineTokenization {292const stateBeforeLine = this._getStateBeforeLine(lineNumber);293294const tokenizationResult1 = this._tokenizationSupport.tokenize(this._model.getLineContent(lineNumber), true, stateBeforeLine);295const tokenizationResult2 = this._tokenizationSupport.tokenizeEncoded(this._model.getLineContent(lineNumber), true, stateBeforeLine);296297return {298startState: stateBeforeLine,299tokens1: tokenizationResult1.tokens,300tokens2: tokenizationResult2.tokens,301endState: tokenizationResult1.endState302};303}304305private _getStateBeforeLine(lineNumber: number): IState {306let state: IState = this._tokenizationSupport.getInitialState();307308for (let i = 1; i < lineNumber; i++) {309const tokenizationResult = this._tokenizationSupport.tokenize(this._model.getLineContent(i), true, state);310state = tokenizationResult.endState;311}312313return state;314}315316public getDomNode(): HTMLElement {317return this._domNode;318}319320public getPosition(): IContentWidgetPosition {321return {322position: this._editor.getPosition(),323preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE]324};325}326}327328registerEditorContribution(InspectTokensController.ID, InspectTokensController, EditorContributionInstantiation.Lazy);329registerEditorAction(InspectTokens);330331332