import type * as Monaco from 'monaco-editor' export type Tokenizer = 'monarch' | 'standard' export interface EmmetOptions { tokenizer?: Tokenizer } interface Token { readonly offset: number readonly type: string readonly language: string } function isValidEmmetToken(tokens: Token[], index: number, syntax: string, language: string): boolean { const currentToken = tokens[index] const currentTokenType = currentToken.type if (syntax === 'html') { // prevent emmet triggered within attributes return ( (currentTokenType === '' && (index === 0 || tokens[index - 1].type === 'delimiter.html')) || // #7 compatible with https://github.com/NeekSandhu/monaco-textmate tokens[0].type === 'text.html.basic' ) } if (syntax === 'css') { if (currentTokenType === '') return true // less / scss allow nesting return currentTokenType === 'tag.' + language } if (syntax === 'jsx') { if (currentToken.language === 'mdx' && currentTokenType === '') { return true } // type must be `identifier` and not at start return ( !!index && ['identifier.js', 'type.identifier.js', 'identifier.ts', 'type.identifier.ts'].includes(currentTokenType) ) } return false } const tokenEnvCache = new WeakMap() function getTokenizationEnv(model: any) { if (tokenEnvCache.has(model)) return tokenEnvCache.get(model)! let _tokenization = // monaco-editor < 0.34.0 model._tokenization || // monaco-editor >= 0.35.0 model.tokenization._tokenization // monaco-editor <= 0.34.0 let _tokenizationStateStore = _tokenization?._tokenizationStateStore // monaco-editor >= 0.35.0 if (!_tokenization || !_tokenizationStateStore) { const _t = model.tokenization const _tokens = // monaco-editor <= 0.51.0 _t.grammarTokens || // monaco-editor 0.52.0 _t._tokens || // monaco-editor >= 0.53.0 _t.tokens?._value if (_tokens) { _tokenization = _tokens._defaultBackgroundTokenizer _tokenizationStateStore = _tokenization._tokenizerWithStateStore } else { // monaco-editor >= 0.35.0 && < 0.37.0, source code was minified Object.values(_t).some((val: any) => (_tokenization = val.tokenizeViewport && val)) Object.values(_tokenization).some((val: any) => (_tokenizationStateStore = val.tokenizationSupport && val)) } } const _tokenizationSupport = // monaco-editor >= 0.32.0 _tokenizationStateStore.tokenizationSupport || // monaco-editor <= 0.31.0 _tokenization._tokenizationSupport const env = { _stateStore: _tokenizationStateStore, _support: _tokenizationSupport, } tokenEnvCache.set(model, env) return env } // When a non-Monarch grammar (e.g. shiki / TextMate) is active, the internal // Monarch token types are unavailable. Instead we use Monaco's public // tokenization API which exposes StandardTokenType (Comment, String, RegEx) // regardless of the underlying grammar engine. function isValidLocationStandard( model: Monaco.editor.ITextModel, position: Monaco.Position, syntax: string, language: string, ): boolean { const tokenization = (model as any).tokenization; if (typeof tokenization?.getLineTokens !== 'function') { console.warn('emmet-monaco-es: Standard tokenizer may not be supported in this version of monaco-editor. Falling back to Monarch tokenizer for emmet abbreviation detection.') return isValidLocationMonarch(model, position, syntax, language) } const { column, lineNumber } = position const lineTokens = tokenization.getLineTokens(lineNumber) let tokenIndex = -1 for (let i = lineTokens.getCount() - 1; i >= 0; i--) { if (column - 1 > lineTokens.getStartOffset(i)) { tokenIndex = i break } } if (tokenIndex < 0) return false // StandardTokenType: Other=0, Comment=1, String=2, RegEx=3 const standardType = lineTokens.getStandardTokenType(tokenIndex) if (standardType !== 0) return false return true } // vscode did a complex node analysis, we just use monaco's built-in tokenizer // to achieve almost the same effect function isValidLocationMonarch( model: Monaco.editor.ITextModel, position: Monaco.Position, syntax: string, language: string, ): boolean { const { column, lineNumber } = position // get current line's tokens const { _stateStore, _support } = getTokenizationEnv(model) // monaco-editor < 0.37.0 uses `getBeginState` while monaco-editor >= 0.37.0 uses `getStartState` // note: lineNumber difference between two api const state = _stateStore.getBeginState?.(lineNumber - 1).clone() || _stateStore.getStartState(lineNumber).clone() const tokenizationResult = _support.tokenize(model.getLineContent(lineNumber), true, state, 0) const tokens: Token[] = tokenizationResult.tokens let valid = false // get token type at current column for (let i = tokens.length - 1; i >= 0; i--) { if (column - 1 > tokens[i].offset) { valid = isValidEmmetToken(tokens, i, syntax, language) break } } return valid } export function isValidLocationForEmmetAbbreviation( model: Monaco.editor.ITextModel, position: Monaco.Position, syntax: string, language: string, options?: EmmetOptions, ) { if (options?.tokenizer === 'standard') { return isValidLocationStandard(model, position, syntax, language) } return isValidLocationMonarch(model, position, syntax, language) }