import { IThemedToken } from 'shiki' ; import { IShikiTheme } from 'shiki-themes' ; import { Highlighter } from 'shiki/dist/highlighter' ; import { HtmlRendererOptions } from 'shiki/dist/renderer' ; import * as vscode from 'vscode' ; import { map , strings } from 'vscode-common' ; import { makeCommandURI } from '../../common' ; import { BuiltInCommands } from '../../constants' ; import { Workspace } from '../../proto/build/stack/bezel/v1beta1/Workspace' ; import { LineBlock } from '../../proto/build/stack/codesearch/v1beta1/LineBlock' ; import { LineBounds } from '../../proto/build/stack/codesearch/v1beta1/LineBounds' ; import { MergedCodeSearchResult } from '../../proto/build/stack/codesearch/v1beta1/MergedCodeSearchResult' ; import { MergedSearchResult } from '../../proto/build/stack/codesearch/v1beta1/MergedSearchResult' ; import { Bounds } from '../../proto/livegrep/Bounds' ; import { CodeSearchResult } from '../../proto/livegrep/CodeSearchResult' ; import { FileResult } from '../../proto/livegrep/FileResult' ; import { Query } from '../../proto/livegrep/Query' ; import { SearchResult } from '../../proto/livegrep/SearchResult' ; import { CodeHighlighter , getLanguageId } from './highlighter' ; import path = require ( 'path' ) ; import Long = require ( 'long' ) ; export class CodesearchRenderer { private readonly _disposables : vscode . Disposable [ ] = [ ] ; private readonly _highlighter : CodeHighlighter ; public readonly needsRender : vscode . Event < void > ; constructor ( ) { this . _highlighter = new CodeHighlighter ( ) ; this . _disposables . push ( this . _highlighter ) ; this . needsRender = this . _highlighter . needsRender ; } dispose ( ) { let item ; while ( ( item = this . _disposables . pop ( ) ) ) { item . dispose ( ) ; } } public async renderSummary ( query , result ) { const atLimit = query . maxMatches === result . results ?. length ; let html = '' ; if ( result . results ) { html += `<span>${ result . results ?. length }${ atLimit ? '+' : '' } match${ result . results . length > 1 ? 'es' : '' }</span>` ; } if ( result . fileResults ) { html += ` (<span>${ result . fileResults ?. length } filename match${ result . fileResults . length > 1 ? 'es' : '' }</span>)` ; } if ( html === '' ) { html = 'No results.' ; } return html ; } public async renderResults ( result , workspace ) { const merge = mergeCodeSearchResult ( result ) ; const highlighter = await this . _highlighter . getHighlighter ( ) ; const theme = this . _highlighter . getCurrentTheme ( ) ; if ( ! ( highlighter && theme ) ) { return 'N/A' ; } let lines = [ ] ; if ( result . fileResults ) { this . formatFileResults ( lines , workspace , result . fileResults , theme ) ; } merge . results ! . forEach ( result => { this . formatMergedSearchResult ( lines , workspace , result , highlighter , theme ) ; } ) ; return lines . join ( '\n' ) ; } private formatFileResults ( lines , workspace , results , theme ) { lines . push ( '<div class="file-results">' ) ; for ( const result of results ) { const filename = result . path ! ; lines . push ( `<div class="linerow file-result" data-file="${ filename }" data-line="0" data-col="0" onclick="postDataElementClick('line', this)">` ) ; if ( result . bounds ) { const pre = filename . slice ( 0 , result . bounds . left ) ; const mid = filename . slice ( result . bounds . left , result . bounds . right ) ; const post = filename . slice ( result . bounds . right ) ; lines . push ( `${ pre }<span class="matchHighlight">${ mid }</span>${ post }` ) ; } else { lines . push ( filename ) ; } lines . push ( '</div>' ) ; } lines . push ( '</div>' ) ; } private formatMergedSearchResult ( lines , workspace , result , highlighter , theme ) { let lineNo = 0 ; if ( result . block && result . block . length && result . block [ 0 ] . lines && result . block [ 0 ] . lines . length ) { lineNo = Long . fromValue ( result . block ! [ 0 ] . lines ! [ 0 ] . lineNumber ! ) . toInt ( ) ; } const openCommand = getVscodeOpenCommand ( result . path ! , lineNo , 0 ) ; const lang = getLanguageFromFilename ( result . path ! ) ; const baseName = path . basename ( result . path || '' ) ; lines . push ( '<div class="peek-view">' ) ; lines . push ( `
		<div class="peek-view-title">
			<label>${ baseName }</label>
			<span class="peek-view-title-description">${ getDisplayFilename ( path . dirname ( result . path ! ) , workspace ) }</span>
			<span style="float: right; margin-right: 0.5rem" class="peek-view-title-description">${ getDisplayLanguageName ( lang , baseName ) }</span>
		</div>
		` ) ; lines . push ( '<div class="file-block">' ) ; for ( const block of result . block ! ) { lines . push ( '<table class="result-block">' ) ; for ( const line of block . lines || [ ] ) { const lineNo = Long . fromValue ( line . lineNumber || 0 ) . toInt ( ) ; lines . push ( `<tr class="linerow" onclick="postDataElementClick('line', this)" data-file="${ result . path }" data-line="${ lineNo }" data-col="0"><td class="lineno ${ line . bounds ?. length ? 'activelineno' : '' }">${ lineNo }</td><td class="line-container">` ) ; const html = formatLineBounds ( line , lang , highlighter , theme ) ; lines . push ( html ) ; lines . push ( '</td></tr>' ) ; } lines . push ( '</table>' ) ; } lines . push ( '</div></div>' ) ; } } function getDisplayLanguageName ( lang , baseName ) { if ( lang === 'php' ) { return '*' ; } if ( lang === 'python' ) { if ( baseName . startsWith ( 'BUILD' ) ) { return 'starlark' ; } } return lang ; } function getDisplayFilename ( resultPath , workspace ) { if ( ! workspace ) { return resultPath ; } let filename = strings . ltrim ( resultPath , workspace . outputBase ! ) ; filename = strings . ltrim ( filename , workspace . cwd ! ) ; if ( filename !== resultPath ) { resultPath = strings . ltrim ( filename , '/' ) ; } return resultPath ; } function getLanguageFromFilename ( filename ) { let lang = path . extname ( filename ) || path . basename ( filename ) ; if ( lang . startsWith ( '.' ) ) { lang = lang . slice ( 1 ) ; } return getLanguageId ( lang ) || 'php' ; } function formatLineBounds ( line , lang , highlighter , theme ) { const lines = highlighter . codeToThemedTokens ( line . line ! , lang ) ; if ( ! ( line . bounds && line . bounds . length ) ) { return renderToHtml ( lines , { bg : 'inherit' , } ) ; } const fg = theme . fg ; const bg = 'inherit' ; let html = '' ; html += `<pre class="shiki" style="background-color: ${ bg }">` ; html += '<code>' ; const bounds = line . bounds ; let boundIndex = 0 ; let cursor = 0 ; const renderTokenContent = ( content , color , clazz = '' ) : string => { return `<span class="${ clazz }" style="color: ${ color || fg }">${ escapeHtml ( content ) }</span>` ; } ; for ( const l of lines ) { if ( l . length === 0 ) { html += '\n' ; } else { html += '<span class="line">' ; for ( const token of l ) { const bound = bounds [ boundIndex ] ; if ( ! bound ) { html += renderTokenContent ( token . content , token . color ) ; continue ; } const nextCursor = cursor + token . content . length ; if ( nextCursor < bound . left ! ) { html += renderTokenContent ( token . content , token . color ) ; } else { const start = bound . left ! - cursor ; const end = bound . right ! - cursor ; const pre = token . content . slice ( 0 , start ) ; const mid = token . content . slice ( start , end ) ; const post = token . content . slice ( end ) ; if ( pre ) { html += renderTokenContent ( pre , token . color ) ; } if ( mid ) { html += renderTokenContent ( mid , token . color , 'matchHighlight' ) ; } if ( post ) { html += renderTokenContent ( post , token . color ) ; } } cursor = nextCursor ; if ( cursor > bound . right ! ) { boundIndex ++ ; } } html += '</span>\n' ; } } html = html . replace ( /\n*$/ , '' ) ; html += '</code></pre>' ; return html ; } export function renderToHtml ( lines , options = { } ) { const bg = options . bg || '#fff' ; let html = '' ; html += `<pre class="shiki" style="background-color: ${ bg }">` ; if ( options . langId ) { html += `<div class="language-id">${ options . langId }</div>` ; } html += '<code>' ; lines . forEach ( ( l ) => { if ( l . length === 0 ) { html += '\n' ; } else { html += '<span class="line">' ; l . forEach ( token => { html += `<span style="color: ${ token . color || options . fg }">${ escapeHtml ( token . content ) }</span>` ; } ) ; html += '</span>\n' ; } } ) ; html = html . replace ( /\n*$/ , '' ) ; html += '</code></pre>' ; return html ; } function getVscodeOpenCommand ( filename , line , col ) { return makeCommandURI ( BuiltInCommands . Open , vscode . Uri . file ( filename ) . with ( { fragment : [ line , col ] . join ( ',' ) , } ) . toString ( ) ) ; } param { ! CodeSearchResult } result returns { ! MergedCodeSearchResult } function mergeCodeSearchResult ( result ) { const msrs = new map . LinkedMap < string , MergedSearchResult > ( ) ; const msrLines = new Map < string , Map < number , LineBounds > > ( ) ; for ( const sr of result . results || [ ] ) { const key = `${ sr . tree || '' }-${ sr . version || '' }-${ sr . path }` ; let msr = msrs . get ( key ) ; if ( ! msr ) { msr = { tree : sr . tree , version : sr . version , path : sr . path , block : [ ] , } ; msrs . set ( key , msr ) ; msrLines . set ( key , new Map ( ) ) ; } let lines = msrLines . get ( key ) ; if ( ! lines ) { lines = new Map ( ) ; } addContextLineBounds ( sr , sr . contextBefore || [ ] , lines , true ) ; addContextLineBounds ( sr , sr . contextAfter || [ ] , lines , false ) ; let lineNo = Long . fromValue ( sr . lineNumber || 0 ) . toInt ( ) ; let bounds = lines . get ( lineNo ) ; if ( ! bounds ) { bounds = { lineNumber : lineNo , line : sr . line , bounds : [ ] , } ; lines . set ( lineNo , bounds ) ; } const bound = sr . bounds ; if ( bound ) { bounds . bounds ! . push ( bound ) ; } } const merge = { fileResults : result . fileResults , results : [ ] , } ; msrs . forEach ( ( msr , key ) => { const lines = msrLines . get ( key ) ! ; const lineNumbers = Array . from ( lines . keys ( ) ) ; lineNumbers . sort ( ( a , b ) => a - b ) ; /** The current line block
         */ let block = undefined ; let lastLineNo = - 1 ; lineNumbers . forEach ( lineNo => { if ( lineNo - lastLineNo !== 1 ) { block = { lines : [ ] , } ; msr . block ! . push ( block ) ; } lastLineNo = lineNo ; const lineBound = lines . get ( lineNo ) ! ; const sortedBounds = sortBoundsList ( lineBound ?. bounds || [ ] ) ; if ( sortedBounds ) { lineBound . bounds = sortedBounds ; } if ( block ) { block . lines ! . push ( lineBound ) ; } } ) ; merge . results ! . push ( msr ) ; } ) ; return merge ; } param { ! SearchResult } sr param { ! Array < string > } context param { ! Map < number , ! LineBounds > } lines param { boolean } before function addContextLineBounds ( sr , context , lines , before ) { for ( let i = 0 ; i < context . length ; i ++ ) { let lineNo = Long . fromValue ( sr . lineNumber || 0 ) . toInt ( ) + i ; if ( before ) { lineNo -= context . length ; } else { lineNo += 1 ; } if ( lines . has ( lineNo ) ) { continue ; } const bounds = { lineNumber : lineNo , line : context [ i ] , bounds : [ ] , } ; lines . set ( lineNo , bounds ) ; } } param { ! Array < ! Bounds > } bounds returns { ! Array < ! Bounds > } function sortBoundsList ( bounds ) { if ( bounds . length < 2 ) { return bounds ; } const m = new Map < number , Bounds > ( ) ; for ( const b of bounds ) { const key = b . left ! + b . right ! ; if ( m . has ( key ) ) { continue ; } m . set ( key , b ) ; } const keys = Array . from ( m . keys ( ) ) ; keys . sort ( ( a , b ) => a - b ) ; return keys . map ( k => m . get ( k ) ! ) ; } const htmlEscapes = { '&' : '&amp;' , '<' : '&lt;' , '>' : '&gt;' , '"' : '&quot;' , "'" : '&#39;' , } ; function escapeHtml ( html ) { return html . replace ( /[&<>"']/g , chr => htmlEscapes [ chr ] ) ; }