import { Position , Range } from '@sourcegraph/extension-api-types' ; import { isEqual } from 'lodash' ; import { isPosition , LineOrPositionOrRange } from './types' ; /**
 * A collection of methods needed to tell codeintellify how to look at the DOM. These are required for
 * ensuring that we don't rely on any sort of specific DOM structure.
 */ export interface DOMFunctions { param target returns getCodeElementFromTarget : ( target : HTMLElement ) => HTMLElement | null ; param codeView param part returns getCodeElementFromLineNumber : ( codeView : HTMLElement , line : number , part ? : DiffPart ) => HTMLElement | null ; param codeElement returns getLineNumberFromCodeElement : ( codeElement : HTMLElement ) => number ; param codeElement returns getDiffCodePart ? : ( codeElement : HTMLElement ) => DiffPart ; param codeElement isFirstCharacterDiffIndicator ? ( codeElement ) : boolean ; } param element export function convertCodeElementIdempotent ( element ) { if ( element && ! element . classList . contains ( 'annotated' ) ) { convertNode ( element ) ; element . classList . add ( 'annotated' ) ; } } /**
 * Helper function to determine whether the code view should be tokenized.
 */ export function shouldTokenize ( { tokenize , overrideTokenize , } ) { return typeof overrideTokenize === 'boolean' ? overrideTokenize : tokenize ; } param parentNode export function convertNode ( parentNode ) { for ( let i = 0 ; i < parentNode . childNodes . length ; ++ i ) { const node = parentNode . childNodes [ i ] ; const isLastNode = i === parentNode . childNodes . length - 1 ; if ( node . nodeType === Node . TEXT_NODE ) { let nodeText = node . textContent ?? '' ; if ( nodeText === '' ) { continue ; } parentNode . removeChild ( node ) ; let insertBefore = i ; while ( true ) { const nextToken = consumeNextToken ( nodeText ) ; if ( nextToken === '' ) { break ; } const newTextNode = document . createTextNode ( nextToken ) ; const newTextNodeWrapper = document . createElement ( 'SPAN' ) ; newTextNodeWrapper . appendChild ( newTextNode ) ; if ( isLastNode ) { parentNode . appendChild ( newTextNodeWrapper ) ; } else { parentNode . insertBefore ( newTextNodeWrapper , parentNode . childNodes [ insertBefore ++ ] ) ; } nodeText = nodeText . substr ( nextToken . length ) ; } } else if ( node . nodeType === Node . ELEMENT_NODE ) { const elementNode = node as HTMLElement ; if ( elementNode . children . length > 0 || ( elementNode . textContent && elementNode . textContent . trim ( ) . length ) ) { convertNode ( elementNode ) ; } } } } const VARIABLE_TOKENIZER = /(^\w+)/ ; const ASCII_CHARACTER_TOKENIZER = /(^[\x21-\x2F|\x3A-\x40|\x5B-\x60|\x7B-\x7E])/ ; const NONVARIABLE_TOKENIZER = /(^[^\x21-\x7E]+)/ ; const enum TokenType { /** Tokens that are alphanumeric, i.e. variable names, keywords */ Alphanumeric , /** Tokens that are ascii characters but aren't in identies (i.e. {, }, [, ], |, ;,  etc) */ ASCII , /** Every token we encounter that doesn't fall into the other two TokenTypes */ Other } param node function getTokenType ( node ) { const text = node . textContent ?? '' ; if ( text . length === 0 ) { return TokenType . Other ; } const variableMatch = text . match ( VARIABLE_TOKENIZER ) ; if ( variableMatch ) { return TokenType . Alphanumeric ; } const asciiMatch = text . match ( ASCII_CHARACTER_TOKENIZER ) ; if ( asciiMatch ) { return TokenType . ASCII ; } return TokenType . Other ; } /**
 * Checks to see if the TokenType of node is the same as the provided token type.
 *
 * When tokenizing the DOM, alphanumeric characters are grouped because they are identities.
 *
 * We also group whitespace just in case. See `consumeNextToken` comments for more information.
 * This is a helper function for making sure the node is the same type of a token and if we care
 * about grouping the type of token together.
 */ function isSameTokenType ( tokenType , node ) { if ( tokenType === TokenType . ASCII ) { return false ; } return tokenType === getTokenType ( node ) ; } param txt function consumeNextToken ( txt ) { if ( txt . length === 0 ) { return '' ; } const variableMatch = txt . match ( VARIABLE_TOKENIZER ) ; if ( variableMatch ) { return variableMatch [ 0 ] ; } const asciiMatch = txt . match ( ASCII_CHARACTER_TOKENIZER ) ; if ( asciiMatch ) { return asciiMatch [ 0 ] ; } const nonVariableMatch = txt . match ( NONVARIABLE_TOKENIZER ) ; if ( nonVariableMatch ) { return nonVariableMatch [ 0 ] ; } return txt [ 0 ] ; } param node export const getTextNodes = ( node ) : Node [ ] => { if ( node . childNodes . length === 0 && node . TEXT_NODE === node . nodeType && node . nodeValue ) { return [ node ] ; } const nodes = [ ] ; for ( const child of node . childNodes ) { nodes . push ( ... getTextNodes ( child ) ) ; } return nodes ; } ; param codeElement param offset export function findElementWithOffset ( codeElement , { offsetStart , offsetEnd } , tokenize = true ) { if ( tokenize ) { convertCodeElementIdempotent ( codeElement ) ; } const textNodes = getTextNodes ( codeElement ) ; let startNode ; let endNode ; if ( typeof offsetEnd === 'number' && offsetEnd > offsetStart ) { /** How far forward we have looked so far. Starting at one because codeintellify treats positions as being 1-indexed. */ let offsetStep = 1 ; let targetStartNode ; let targetEndNode ; for ( const node of textNodes ) { const text = node . textContent ?? '' ; if ( ! targetStartNode ) { if ( offsetStep <= offsetStart && offsetStep + text . length > offsetStart ) { targetStartNode = node ; } } if ( targetStartNode ) { if ( offsetStep < offsetEnd && offsetStep + text . length >= offsetEnd ) { targetEndNode = node ; break ; } } offsetStep += text . length ; } if ( ! targetStartNode || ! targetEndNode ) { return undefined ; } startNode = targetStartNode ; endNode = targetEndNode ; } else { /** How far forward we have looked so far. Starting at one because codeintellify treats positions as being 1-indexed. */ let offsetStep = 1 ; let nodeIndex = 0 ; let targetNode ; for ( const [ i , node ] of textNodes . entries ( ) ) { const text = node . textContent ?? '' ; if ( offsetStep <= offsetStart && offsetStep + text . length > offsetStart ) { targetNode = node ; nodeIndex = i ; break ; } offsetStep += text . length ; } if ( ! targetNode ) { return undefined ; } const tokenType = getTokenType ( targetNode ) ; param idx param delta const findTokenEdgeIndex = ( idx , delta ) : number => { let at = idx ; while ( textNodes [ at + delta ] && isSameTokenType ( tokenType , textNodes [ at + delta ] ) ) { at += delta ; } return at ; } ; startNode = textNodes [ findTokenEdgeIndex ( nodeIndex , - 1 ) ] ; endNode = textNodes [ findTokenEdgeIndex ( nodeIndex , 1 ) ] ; } const tokenRange = document . createRange ( ) ; tokenRange . setStartBefore ( startNode ) ; tokenRange . setEndAfter ( endNode ) ; if ( startNode === endNode || ( tokenRange . commonAncestorContainer as HTMLElement ) . classList . contains ( 'wrapped' ) ) { return tokenRange . commonAncestorContainer as HTMLElement ; } const wrapper = document . createElement ( 'span' ) ; wrapper . classList . add ( 'wrapped' ) ; wrapper . appendChild ( tokenRange . extractContents ( ) ) ; tokenRange . insertNode ( wrapper ) ; return wrapper ; } /**
 * Whether a line belongs to the base rev of the diff (removed), the head (added) or `null` if either (not changed).
 */ export type DiffPart = 'base' | 'head' | null ; /**
 * Returned when only the line is known.
 *
 * 1-indexed
 */ export interface Line { line : number ; } export interface HoveredToken { /** 1-indexed */ line : number ; /** 1-indexed */ character : number ; part ? : DiffPart ; } param target param ignoreFirstChar export function locateTarget ( target , { getCodeElementFromTarget , getLineNumberFromCodeElement , getDiffCodePart , isFirstCharacterDiffIndicator , } ) { const codeElement = getCodeElementFromTarget ( target ) ; if ( ! codeElement ) { return undefined ; } const line = getLineNumberFromCodeElement ( codeElement ) ; if ( target === codeElement ) { return { line } ; } const part = getDiffCodePart ?. ( codeElement ) ; let ignoreFirstCharacter = ! ! isFirstCharacterDiffIndicator && isFirstCharacterDiffIndicator ( codeElement ) ; let character = 1 ; function findOrigTarget ( root ) { if ( root === target ) { return true ; } for ( const child of root . childNodes ) { if ( child === target ) { return true ; } if ( ! ( child instanceof HTMLElement ) ) { character += child . textContent ! . length ; continue ; } if ( child . children . length > 0 && findOrigTarget ( child ) ) { return true ; } if ( child . children . length === 0 ) { character += ( child . textContent as string ) . length ; if ( ignoreFirstCharacter ) { character -= 1 ; ignoreFirstCharacter = false ; } } } return false ; } if ( findOrigTarget ( codeElement ) ) { return { line , character , part } ; } return { line } ; } export interface GetCodeElementsInRangeOptions extends Pick < DOMFunctions , 'getCodeElementFromLineNumber' > { codeView : HTMLElement ; position ? : LineOrPositionOrRange ; part ? : DiffPart ; } export const getCodeElementsInRange = ( { codeView , position , part , getCodeElementFromLineNumber , } ) : { /** 1-indexed line number */ line : number ; /** The element containing the code */ element : HTMLElement ; } [ ] => { if ( ! position || position . line === undefined ) { return [ ] ; } const elements = [ ] ; for ( let line = position . line ; line <= ( position . endLine || position . line ) ; line ++ ) { const element = getCodeElementFromLineNumber ( codeView , line , part ) ; if ( ! element ) { break ; } elements . push ( { line , element } ) ; } return elements ; } ; param codeView param positionOrRange param domOptions param part export const getTokenAtPositionOrRange = ( codeView , positionOrRange , { getCodeElementFromLineNumber , isFirstCharacterDiffIndicator , } , part ? , tokenize = true ) : HTMLElement | undefined => { if ( isPosition ( positionOrRange ) || isEqual ( positionOrRange . start , positionOrRange . end ) || positionOrRange . start . line !== positionOrRange . end . line ) { let { line , character } = isPosition ( positionOrRange ) ? positionOrRange : positionOrRange . start ; const codeElement = getCodeElementFromLineNumber ( codeView , line , part ) ; if ( ! codeElement ) { return undefined ; } if ( isFirstCharacterDiffIndicator ?. ( codeElement ) ) { character ++ ; } return findElementWithOffset ( codeElement , { offsetStart : character } , tokenize ) ; } const { start , end } = positionOrRange ; let { character : startCharacter , line } = start ; let { character : endCharacter } = end ; const codeElement = getCodeElementFromLineNumber ( codeView , line , part ) ; if ( ! codeElement ) { return undefined ; } if ( isFirstCharacterDiffIndicator ?. ( codeElement ) ) { startCharacter ++ ; endCharacter ++ ; } return findElementWithOffset ( codeElement , { offsetStart : startCharacter < endCharacter ? startCharacter : endCharacter , offsetEnd : startCharacter < endCharacter ? endCharacter : startCharacter , } , tokenize ) ; } ;