license import { Analysis , Analyzer , AnalyzerOptions , ResolvedUrl , UrlResolver } from 'polymer-analyzer' ; import { AnalysisCache } from 'polymer-analyzer/lib/core/analysis-cache' ; import { AnalysisContext } from 'polymer-analyzer/lib/core/analysis-context' ; import { AnalyzeOptions } from 'polymer-analyzer/lib/core/analyzer' ; import { FileChangeType , FileEvent , TextDocuments } from 'vscode-languageserver' ; import AnalyzerLSPConverter from './converter' ; import FileSynchronizer from './file-synchronizer' ; import { Logger } from './logger' ; import { AutoDisposable , EventStream } from './util' ; /**
 * Exposes an Analyzer that is always in sync with the client's state,
 * and that can give the versions of open files when a given Analysis was
 * generated.
 */ export default class AnalyzerSynchronizer extends AutoDisposable { public readonly analyzer : LsAnalyzer ; /**
     * An event stream that fires every time files have changed such that
     * re-analyzing may give different results.
     *
     * We don't expose the list of files changed here because the file the
     * user is interested in may depend on one of the files changed. The
     * Analyzer is smart about this, so we'll return cached results almost
     * instantly if nothing has actually changed.
     */ public readonly analysisChanges : EventStream < void > ; constructor ( private readonly documents , fileSynchronizer , logger , urlResolver , private readonly converter ) { super ( ) ; const { fire , stream } = EventStream . create < void > ( ) ; this . analysisChanges = stream ; const analysisVersionMap = new WeakMap < Analysis , Map < string , number > > ( ) ; this . analyzer = new LsAnalyzer ( this . documents , logger , analysisVersionMap , { urlLoader : fileSynchronizer . urlLoader , urlResolver , } ) ; this . disposables . push ( fileSynchronizer . fileChanges . listen ( ( filesChangeEvents ) => { this . handleFilesChanged ( filesChangeEvents , fire ) ; } ) ) ; } private async handleFilesChanged ( fileChangeEvents , onComplete ) { const uris = fileChangeEvents . map ( ( f ) => f . uri ) ; fileChangeEvents = fileChangeEvents . filter ( ( change ) => this . converter . getWorkspacePathToFile ( change ) !== 'polymer.json' ) ; if ( fileChangeEvents . length === 0 ) { return ; } const deletedUris = fileChangeEvents . filter ( ( change ) => change . type === FileChangeType . Deleted ) . map ( ( change ) => change . uri ) ; if ( deletedUris . length > 0 ) { try { const context = await this . analyzer [ '_analysisComplete' ] ; const cache = context [ '_cache' ] ; const cachedUris = new Set < ResolvedUrl > ( [ ... cache . failedDocuments . keys ( ) , ... cache . parsedDocumentPromises [ '_keyToResultMap' ] . keys ( ) ] ) ; for ( const deletedUri of deletedUris ) { const deletedDir = deletedUri + '/' ; for ( const cachedUri of cachedUris ) { if ( cachedUri . startsWith ( deletedDir ) ) { uris . push ( cachedUri ) ; } } } } catch { } } const filesChangedPromise = this . analyzer . filesChanged ( uris ) ; onComplete ( undefined ) ; await filesChangedPromise ; } } export interface LsAnalyzeOptions extends AnalyzeOptions { reason ? : string ; } /**
 * An extension of the analyzer that's aware of the LSP versions of
 * in-memory files at the time an analysis is generated.
 */ export class LsAnalyzer extends Analyzer { constructor ( private documents , private logger , private analysisVersionMap , options ) { super ( options ) ; } analyze ( files , options = { } ) { const prefix = options . reason ? `${ options . reason }: ` : '' ; this . logger . log ( `${ prefix }Analyzing files: ${ files . join ( ', ' ) }` ) ; const start = ( new Date ( ) . getTime ( ) ) ; const result = this . annotateWithVersionMap ( super . analyze ( files , options ) ) ; result . then ( ( ) => { const end = ( new Date ( ) . getTime ( ) ) ; const elapsed = ( ( end - start ) / 1000 ) . toFixed ( 2 ) ; this . logger . log ( `${ prefix }Took ${ elapsed }s to analyze files: ${ files . join ( ', ' ) }` ) ; } ) ; return result ; } analyzePackage ( options = { } ) { const prefix = options . reason ? `${ options . reason }: ` : '' ; this . logger . log ( `${ prefix }Analyzing package...` ) ; const start = ( new Date ( ) . getTime ( ) ) ; const result = this . annotateWithVersionMap ( super . analyzePackage ( options ) ) ; result . then ( ( ) => { const end = ( new Date ( ) . getTime ( ) ) ; const elapsed = ( ( end - start ) / 1000 ) . toFixed ( 2 ) ; this . logger . log ( `${ prefix }:Took ${ elapsed }s to analyze package.` ) ; } ) ; return result ; } private async annotateWithVersionMap ( promise ) { const versionMap = new Map < string , number > ( ) ; for ( const document of this . documents . all ( ) ) { versionMap . set ( document . uri , document . version ) ; } const analysis = await promise ; this . analysisVersionMap . set ( analysis , versionMap ) ; return analysis ; } /**
     * Gives a map from URI to version number for all open files at the moment
     * that the given Analysis was generated.
     *
     * This is useful for getting versions right when applying edits.
     */ getVersionsAtAnalysis ( analysis ) { return this . analysisVersionMap . get ( analysis ) ! ; } }