import Long = require ( 'long' ) ; import * as vscode from 'vscode' ; import * as grpc from '@grpc/grpc-js' ; import { Scope } from '../proto/build/stack/codesearch/v1beta1/Scope' ; import { QueryOptions } from './codesearch/constants' ; import { Query } from '../proto/livegrep/Query' ; import { getRelativeDateFromTimestamp , md5Hash } from '../common' ; import { event } from 'vscode-common' ; import { Duration } from 'luxon' ; import { CreateScopeResponse } from '../proto/build/stack/codesearch/v1beta1/CreateScopeResponse' ; import { CreateScopeRequest } from '../proto/build/stack/codesearch/v1beta1/CreateScopeRequest' ; import { Container } from '../container' ; import { CommandName } from './constants' ; import { CodesearchRenderer } from './codesearch/renderer' ; import { CodesearchPanel , CodesearchRenderProvider , Message } from './codesearch/panel' ; import { BuiltInCommands } from '../constants' ; import { Bzl } from './bzl' ; import { DisabledError , RunnableComponent , Status } from './status' ; import { CodeSearchConfiguration , CodeSearchSettings } from './configuration' ; /**
 * CodesearchIndexOptions describes options for the index command.
 */ export interface CodesearchIndexOptions { args : string [ ] ; cwd : string ; } export interface OutputChannel { clear ( ) : void ; show ( ) : void ; appendLine ( line ) : void ; } /**
 * Codesarch implements a panel for codesarching.
 */ export class CodeSearch extends RunnableComponent < CodeSearchConfiguration > implements vscode . Disposable { private readonly output : vscode . OutputChannel ; private readonly renderer : CodesearchRenderer ; private panel : CodesearchPanel | undefined ; constructor ( settings , public readonly bzl ) { super ( 'CS0' , settings ) ; bzl . onDidChangeStatus ( this . restart , this , this . disposables ) ; this . output = vscode . window . createOutputChannel ( 'Codesearch' ) ; this . renderer = new CodesearchRenderer ( ) ; this . disposables . push ( this . output ) ; this . disposables . push ( this . renderer ) ; this . disposables . push ( vscode . commands . registerCommand ( CommandName . Codesearch , this . handleCommandCodesearch , this ) ) ; this . disposables . push ( vscode . commands . registerCommand ( CommandName . CodesearchIndex , this . handleCodeIndex , this ) ) ; this . disposables . push ( vscode . commands . registerCommand ( CommandName . CodesearchSearch , this . handleCodeSearchCommand , this ) ) ; } async startInternal ( ) { if ( this . bzl . status !== Status . READY ) { throw new DisabledError ( 'Bzl not ready' ) ; } } async stopInternal ( ) { } async handleCommandCodesearch ( label ) { const ws = await this . bzl . getWorkspace ( ) ; const expr = `deps(${ label })` ; vscode . commands . executeCommand ( CommandName . CodesearchSearch , { cwd : ws . cwd , args : [ expr ] , } ) ; } getOrCreateSearchPanel ( queryExpression ) { if ( ! this . panel ) { this . panel = new CodesearchPanel ( this . settings . configCtx . extensionUri , 'Codesearch' , `Codesearch ${ queryExpression }` , vscode . ViewColumn . One ) ; this . panel . onDidDispose ( ( ) => { this . panel = undefined ; } , this , this . disposables ) ; } return this . panel ; } async handleCodeIndex ( opts ) { await this . createScope ( opts , this . output ) ; return this . handleCodeSearch ( opts ) ; } async createScope ( opts , output ) { const client = this . bzl . client ; if ( ! client ) { return ; } const cfg = await this . bzl . settings . get ( ) ; const ws = await this . bzl . getWorkspace ( ) ; if ( ! ( ws . cwd && ws . outputBase ) ) { return ; } let command = 'query' ; const query = [ ] ; const options = [ ] ; let sink = query ; for ( let i = 0 ; i < opts . args . length ; i ++ ) { const arg = opts . args [ i ] ; if ( i === 0 && ( arg === 'query' || arg === 'cquery' ) ) { command = arg ; continue ; } if ( arg === '--' ) { sink = options ; continue ; } sink . push ( arg ) ; } const queryExpression = opts . args . join ( ' ' ) ; const scopeName = md5Hash ( queryExpression ) ; const request = { cwd : ws . cwd , outputBase : ws . outputBase , name : scopeName , bazelQuery : { command : command , expression : query . join ( ' ' ) , options : options , } , } ; output . clear ( ) ; output . show ( ) ; output . appendLine ( `Indexing ${ queryExpression }...` ) ; return vscode . window . withProgress < void > ( { location : vscode . ProgressLocation . Notification , title : `Indexing ${ queryExpression }...` , cancellable : false , } , async ( progress , token ) : Promise < void > => { return client . createScope ( request , async ( response ) => { if ( response . progress ) { for ( const line of response . progress || [ ] ) { output . appendLine ( line ) ; progress . report ( { message : line } ) ; } } } ) ; } ) . then ( ( ) => vscode . commands . executeCommand ( BuiltInCommands . ClosePanel ) ) ; } async handleCodeSearchCommand ( opts ) { try { return this . handleCodeSearch ( opts ) ; } catch ( e ) { if ( e instanceof Error ) { vscode . window . showErrorMessage ( `could not handle codesearch command: ${ e . message }` ) ; } } } async handleCodeSearch ( opts ) { const client = this . bzl . client ; if ( ! client ) { return ; } const ws = await this . bzl . getWorkspace ( ) ; if ( ! ( ws . cwd && ws . outputBase ) ) { return ; } const cfg = await this . settings . get ( ) ; const bzlCfg = await this . bzl . settings . get ( ) ; const query = { repo : ws . outputBase , file : ws . cwd , foldCase : cfg . foldCase , maxMatches : cfg . maxMatches , contextLines : cfg . defaultLinesContext , tags : QueryOptions . QuoteMeta , } ; const queryExpression = opts . args . join ( ' ' ) ; const scopeName = md5Hash ( queryExpression ) ; let scope = undefined ; try { scope = await client . getScope ( { cwd : ws . cwd , outputBase : ws . outputBase , name : scopeName , } ) ; } catch ( err ) { const e = err as grpc . ServiceError ; if ( e . code && e . code !== grpc . status . NOT_FOUND ) { vscode . window . showErrorMessage ( `getScope: ${ e . message } (${ e . code })` ) ; } } const panel = this . getOrCreateSearchPanel ( queryExpression ) ; const queryChangeEmitter = new event . Emitter < Query > ( ) ; const queryDidChange = event . Event . debounce ( queryChangeEmitter . event , ( last , e ) => e , 250 , true ) ; queryDidChange ( async ( q ) => { const start = Date . now ( ) ; if ( ! q . line ) { panel . onDidChangeHTMLSummary . fire ( 'Searching ' + queryExpression ) ; panel . onDidChangeHTMLResults . fire ( '' ) ; return ; } panel . onDidChangeHTMLSummary . fire ( 'Working...' ) ; panel . onDidChangeHTMLResults . fire ( '<progress></progress>' ) ; const timeoutID = setTimeout ( ( ) => { panel . onDidChangeHTMLSummary . fire ( 'Timed out.' ) ; panel . onDidChangeHTMLResults . fire ( '' ) ; } , 1000 ) ; try { const result = await client . searchScope ( { scopeName : scopeName , query : q , } ) ; clearTimeout ( timeoutID ) ; panel . onDidChangeHTMLSummary . fire ( 'Rendering results...' ) ; const resultsHTML = await this . renderer . renderResults ( result , ws ) ; let summaryHTML = await this . renderer . renderSummary ( q , result ) ; const dur = Duration . fromMillis ( Date . now ( ) - start ) ; summaryHTML += ` [${ dur . milliseconds } ms]` ; panel . onDidChangeHTMLSummary . fire ( summaryHTML ) ; panel . onDidChangeHTMLResults . fire ( resultsHTML ) ; } catch ( e ) { clearTimeout ( timeoutID ) ; const err = e as grpc . ServiceError ; panel . onDidChangeHTMLSummary . fire ( err . message ) ; panel . onDidChangeHTMLResults . fire ( '' ) ; } } ) ; await this . renderSearchPanel ( ws . cwd , queryExpression , scope , panel , query , queryChangeEmitter ) ; if ( ! scope ) { panel . onDidChangeHTMLSummary . fire ( 'Codesearch index has not been created.  Click [Recreate Index] to build it.' ) ; panel . onDidChangeHTMLResults . fire ( '' ) ; return ; } if ( ! scope . size ) { panel . onDidChangeHTMLSummary . fire ( 'Codesearch index contains no files.  Try [Recreate Index] to (re)build it.' ) ; panel . onDidChangeHTMLResults . fire ( '' ) ; return ; } panel . onDidChangeHTMLSummary . fire ( `Searching ${ scope . size } files [${ queryExpression }]` ) ; } async renderSearchPanel ( cwd , queryExpression , scope , panel , query , queryChangeEmitter ) { let lastIndexed = 'never' ; let files = 0 ; if ( scope ) { files = Long . fromValue ( scope . size || 0 ) . toInt ( ) ; if ( scope . createdAt ) { lastIndexed = getRelativeDateFromTimestamp ( scope . createdAt ) ; } } let heading = `codesearch <span class="text-hl">${ files }</span> files, last indexed <span class="text-hl">${ lastIndexed }</span>` ; return panel . render ( { heading : heading , form : { name : 'search' , buttons : [ { label : 'Recreate Index' , name : 'index' , secondary : true , onclick : async ( ) => { vscode . commands . executeCommand ( CommandName . CodesearchIndex , { cwd : cwd , args : [ queryExpression ] , } ) ; } , } , ] , inputs : [ { label : 'Query' , type : 'text' , name : 'number' , placeholder : 'Search expression' , display : 'inline-block' , size : 40 , autofocus : true , onchange : async ( value ) => { if ( ! value || value . length < 3 ) { query . line = '' ; queryChangeEmitter . fire ( query ) ; return ; } query . line = value ; queryChangeEmitter . fire ( query ) ; return '' ; } , } , { label : 'Max Matches' , type : 'number' , name : 'max' , value : '50' , display : 'inline-block' , maxlength : 3 , size : 3 , onchange : async ( value ) => { if ( ! value ) { return ; } query . maxMatches = parseInt ( value , 10 ) ; queryChangeEmitter . fire ( query ) ; return '' ; } , } , { label : 'Lines Context' , type : 'number' , name : 'context' , value : '3' , maxlength : 3 , display : 'inline-block' , size : 2 , onchange : async ( value ) => { if ( ! value ) { return ; } query . contextLines = parseInt ( value , 10 ) ; queryChangeEmitter . fire ( query ) ; return '' ; } , } , { label : 'Regexp' , type : 'checkbox' , name : 'regexp' , style : 'vertical-align: top' , display : 'inline-block' , onchange : async ( value ) => { if ( ! value ) { return ; } query . tags = value === 'on' ? '' : QueryOptions . QuoteMeta ; queryChangeEmitter . fire ( query ) ; return '' ; } , } , ] , } , callbacks : { 'click.line' : ( m ) => { if ( ! m . data ) { return ; } const filename = m . data [ 'file' ] ; const line = m . data [ 'line' ] ; const col = m . data [ 'col' ] ; if ( ! ( filename && line && col ) ) { return ; } vscode . commands . executeCommand ( BuiltInCommands . Open , vscode . Uri . file ( filename ) . with ( { fragment : `${ line },${ col }` , } ) ) ; } , } , } ) ; } }