namespace ts . server { export interface ScriptInfoVersion { svc : number ; text : number ; } export class TextStorage { version : ScriptInfoVersion ; /**
         * Generated only on demand (based on edits, or information requested)
         * The property text is set to undefined when edits happen on the cache
         */ private svc : ScriptVersionCache | undefined ; /**
         * Stores the text when there are no changes to the script version cache
         * The script version cache is generated on demand and text is still retained.
         * Only on edits to the script version cache, the text will be set to undefined
         */ private text : string | undefined ; /**
         * Line map for the text when there is no script version cache present
         */ private lineMap : number [ ] | undefined ; /**
         * When a large file is loaded, text will artificially be set to "".
         * In order to be able to report correct telemetry, we store the actual
         * file size in this case.  (In other cases where text === "", e.g.
         * for mixed content or dynamic files, fileSize will be undefined.)
         */ private fileSize : number | undefined ; /**
         * True if the text is for the file thats open in the editor
         */ public isOpen = false ; /**
         * True if the text present is the text from the file on the disk
         */ private ownFileText = false ; /**
         * True when reloading contents of file from the disk is pending
         */ private pendingReloadFromDisk = false ; constructor ( private readonly host , private readonly info , initialVersion ? ) { this . version = initialVersion || { svc : 0 , text : 0 } ; } public getVersion ( ) { return this . svc ? `SVC-${ this . version . svc }-${ this . svc . getSnapshotVersion ( ) }` : `Text-${ this . version . text }` ; } public hasScriptVersionCache_TestOnly ( ) { return this . svc !== undefined ; } public useScriptVersionCache_TestOnly ( ) { this . switchToScriptVersionCache ( ) ; } private resetSourceMapInfo ( ) { this . info . sourceFileLike = undefined ; this . info . closeSourceMapFileWatcher ( ) ; this . info . sourceMapFilePath = undefined ; this . info . declarationInfoPath = undefined ; this . info . sourceInfos = undefined ; this . info . documentPositionMapper = undefined ; } /** Public for testing */ public useText ( newText ? ) { this . svc = undefined ; this . text = newText ; this . lineMap = undefined ; this . fileSize = undefined ; this . resetSourceMapInfo ( ) ; this . version . text ++ ; } public edit ( start , end , newText ) { this . switchToScriptVersionCache ( ) . edit ( start , end - start , newText ) ; this . ownFileText = false ; this . text = undefined ; this . lineMap = undefined ; this . fileSize = undefined ; this . resetSourceMapInfo ( ) ; } /**
         * Set the contents as newText
         * returns true if text changed
         */ public reload ( newText ) { Debug . assert ( newText !== undefined ) ; this . pendingReloadFromDisk = false ; if ( this . text !== newText ) { this . useText ( newText ) ; this . ownFileText = false ; return true ; } return false ; } /**
         * Reads the contents from tempFile(if supplied) or own file and sets it as contents
         * returns true if text changed
         */ public reloadWithFileText ( tempFileName ? ) { const { text : newText , fileSize } = this . getFileTextAndSize ( tempFileName ) ; const reloaded = this . reload ( newText ) ; this . fileSize = fileSize ; this . ownFileText = ! tempFileName || tempFileName === this . info . fileName ; return reloaded ; } /**
         * Reloads the contents from the file if there is no pending reload from disk or the contents of file are same as file text
         * returns true if text changed
         */ public reloadFromDisk ( ) { if ( ! this . pendingReloadFromDisk && ! this . ownFileText ) { return this . reloadWithFileText ( ) ; } return false ; } public delayReloadFromFileIntoText ( ) { this . pendingReloadFromDisk = true ; } /**
         * For telemetry purposes, we would like to be able to report the size of the file.
         * However, we do not want telemetry to require extra file I/O so we report a size
         * that may be stale (e.g. may not reflect change made on disk since the last reload).
         * NB: Will read from disk if the file contents have never been loaded because
         * telemetry falsely indicating size 0 would be counter-productive.
         */ public getTelemetryFileSize ( ) { return ! ! this . fileSize ? this . fileSize : ! ! this . text ? this . text . length : ! ! this . svc ? this . svc . getSnapshot ( ) . getLength ( ) : this . getSnapshot ( ) . getLength ( ) ; } public getSnapshot ( ) { return this . useScriptVersionCacheIfValidOrOpen ( ) ? this . svc ! . getSnapshot ( ) : ScriptSnapshot . fromString ( this . getOrLoadText ( ) ) ; } public getAbsolutePositionAndLineText ( line ) { return this . switchToScriptVersionCache ( ) . getAbsolutePositionAndLineText ( line ) ; } param line lineToTextSpan ( line ) { if ( ! this . useScriptVersionCacheIfValidOrOpen ( ) ) { const lineMap = this . getLineMap ( ) ; const start = lineMap [ line ] ; const end = line + 1 < lineMap . length ? lineMap [ line + 1 ] : this . text ! . length ; return createTextSpanFromBounds ( start , end ) ; } return this . svc ! . lineToTextSpan ( line ) ; } param line param offset lineOffsetToPosition ( line , offset , allowEdits ? ) { if ( ! this . useScriptVersionCacheIfValidOrOpen ( ) ) { return computePositionOfLineAndCharacter ( this . getLineMap ( ) , line - 1 , offset - 1 , this . text , allowEdits ) ; } return this . svc ! . lineOffsetToPosition ( line , offset ) ; } positionToLineOffset ( position ) { if ( ! this . useScriptVersionCacheIfValidOrOpen ( ) ) { const { line , character } = computeLineAndCharacterOfPosition ( this . getLineMap ( ) , position ) ; return { line : line + 1 , offset : character + 1 } ; } return this . svc ! . positionToLineOffset ( position ) ; } private getFileTextAndSize ( tempFileName ? ) { let text ; const fileName = tempFileName || this . info . fileName ; const getText = ( ) => text === undefined ? ( text = this . host . readFile ( fileName ) || "" ) : text ; if ( ! hasTSFileExtension ( this . info . fileName ) ) { const fileSize = this . host . getFileSize ? this . host . getFileSize ( fileName ) : getText ( ) . length ; if ( fileSize > maxFileSize ) { Debug . assert ( ! ! this . info . containingProjects . length ) ; const service = this . info . containingProjects [ 0 ] . projectService ; service . logger . info ( `Skipped loading contents of large file ${ fileName } for info ${ this . info . fileName }: fileSize: ${ fileSize }` ) ; this . info . containingProjects [ 0 ] . projectService . sendLargeFileReferencedEvent ( fileName , fileSize ) ; return { text : "" , fileSize } ; } } return { text : getText ( ) } ; } private switchToScriptVersionCache ( ) { if ( ! this . svc || this . pendingReloadFromDisk ) { this . svc = ScriptVersionCache . fromString ( this . getOrLoadText ( ) ) ; this . version . svc ++ ; } return this . svc ; } private useScriptVersionCacheIfValidOrOpen ( ) { if ( this . isOpen ) { return this . switchToScriptVersionCache ( ) ; } if ( this . pendingReloadFromDisk ) { this . reloadWithFileText ( ) ; } return this . svc ; } private getOrLoadText ( ) { if ( this . text === undefined || this . pendingReloadFromDisk ) { Debug . assert ( ! this . svc || this . pendingReloadFromDisk , "ScriptVersionCache should not be set when reloading from disk" ) ; this . reloadWithFileText ( ) ; } return this . text ! ; } private getLineMap ( ) { Debug . assert ( ! this . svc , "ScriptVersionCache should not be set" ) ; return this . lineMap || ( this . lineMap = computeLineStarts ( this . getOrLoadText ( ) ) ) ; } getLineInfo ( ) { if ( this . svc ) { return { getLineCount : ( ) => this . svc ! . getLineCount ( ) , getLineText : line => this . svc ! . getAbsolutePositionAndLineText ( line + 1 ) . lineText ! } ; } const lineMap = this . getLineMap ( ) ; return getLineInfo ( this . text ! , lineMap ) ; } } export function isDynamicFileName ( fileName ) { return fileName [ 0 ] === "^" || ( ( stringContains ( fileName , "walkThroughSnippet:/" ) || stringContains ( fileName , "untitled:/" ) ) && getBaseFileName ( fileName ) [ 0 ] === "^" ) || ( stringContains ( fileName , ":^" ) && ! stringContains ( fileName , directorySeparator ) ) ; } export interface DocumentRegistrySourceFileCache { key : DocumentRegistryBucketKey ; sourceFile : SourceFile ; } export interface SourceMapFileWatcher { watcher : FileWatcher ; sourceInfos ? : Set < Path > ; } export class ScriptInfo { /**
         * All projects that include this file
         */ readonly containingProjects : Project [ ] = [ ] ; private formatSettings : FormatCodeSettings | undefined ; private preferences : protocol . UserPreferences | undefined ; fileWatcher : FileWatcher | undefined ; private textStorage : TextStorage ; readonly isDynamic : boolean ; /** Set to real path if path is different from info.path */ private realpath : Path | undefined ; cacheSourceFile : DocumentRegistrySourceFileCache | undefined ; mTime ? : number ; sourceFileLike ? : SourceFileLike ; sourceMapFilePath ? : Path | SourceMapFileWatcher | false ; declarationInfoPath ? : Path ; sourceInfos ? : Set < Path > ; documentPositionMapper ? : DocumentPositionMapper | false ; constructor ( private readonly host , readonly fileName , readonly scriptKind , public readonly hasMixedContent , readonly path , initialVersion ? ) { this . isDynamic = isDynamicFileName ( fileName ) ; this . textStorage = new TextStorage ( host , this , initialVersion ) ; if ( hasMixedContent || this . isDynamic ) { this . textStorage . reload ( "" ) ; this . realpath = this . path ; } this . scriptKind = scriptKind ? scriptKind : getScriptKindFromFileName ( fileName ) ; } getVersion ( ) { return this . textStorage . version ; } getTelemetryFileSize ( ) { return this . textStorage . getTelemetryFileSize ( ) ; } public isDynamicOrHasMixedContent ( ) { return this . hasMixedContent || this . isDynamic ; } public isScriptOpen ( ) { return this . textStorage . isOpen ; } public open ( newText ) { this . textStorage . isOpen = true ; if ( newText !== undefined && this . textStorage . reload ( newText ) ) { this . markContainingProjectsAsDirty ( ) ; } } public close ( fileExists = true ) { this . textStorage . isOpen = false ; if ( this . isDynamicOrHasMixedContent ( ) || ! fileExists ) { if ( this . textStorage . reload ( "" ) ) { this . markContainingProjectsAsDirty ( ) ; } } else if ( this . textStorage . reloadFromDisk ( ) ) { this . markContainingProjectsAsDirty ( ) ; } } public getSnapshot ( ) { return this . textStorage . getSnapshot ( ) ; } private ensureRealPath ( ) { if ( this . realpath === undefined ) { this . realpath = this . path ; if ( this . host . realpath ) { Debug . assert ( ! ! this . containingProjects . length ) ; const project = this . containingProjects [ 0 ] ; const realpath = this . host . realpath ( this . path ) ; if ( realpath ) { this . realpath = project . toPath ( realpath ) ; if ( this . realpath !== this . path ) { project . projectService . realpathToScriptInfos ! . add ( this . realpath , this ) ; } } } } } getRealpathIfDifferent ( ) { return this . realpath && this . realpath !== this . path ? this . realpath : undefined ; } internal isSymlink ( ) { return this . realpath && this . realpath !== this . path ; } getFormatCodeSettings ( ) { return this . formatSettings ; } getPreferences ( ) { return this . preferences ; } attachToProject ( project ) { const isNew = ! this . isAttached ( project ) ; if ( isNew ) { this . containingProjects . push ( project ) ; if ( ! project . getCompilerOptions ( ) . preserveSymlinks ) { this . ensureRealPath ( ) ; } project . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; } return isNew ; } isAttached ( project ) { switch ( this . containingProjects . length ) { case 0 : return false ; case 1 : return this . containingProjects [ 0 ] === project ; case 2 : return this . containingProjects [ 0 ] === project || this . containingProjects [ 1 ] === project ; default : return contains ( this . containingProjects , project ) ; } } detachFromProject ( project ) { switch ( this . containingProjects . length ) { case 0 : return ; case 1 : if ( this . containingProjects [ 0 ] === project ) { project . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; this . containingProjects . pop ( ) ; } break ; case 2 : if ( this . containingProjects [ 0 ] === project ) { project . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; this . containingProjects [ 0 ] = this . containingProjects . pop ( ) ! ; } else if ( this . containingProjects [ 1 ] === project ) { project . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; this . containingProjects . pop ( ) ; } break ; default : if ( unorderedRemoveItem ( this . containingProjects , project ) ) { project . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; } break ; } } detachAllProjects ( ) { for ( const p of this . containingProjects ) { if ( isConfiguredProject ( p ) ) { p . getCachedDirectoryStructureHost ( ) . addOrDeleteFile ( this . fileName , this . path , FileWatcherEventKind . Deleted ) ; } const existingRoot = p . getRootFilesMap ( ) . get ( this . path ) ; p . removeFile ( this , false , false ) ; p . onFileAddedOrRemoved ( this . isSymlink ( ) ) ; if ( existingRoot && ! isInferredProject ( p ) ) { p . addMissingFileRoot ( existingRoot . fileName ) ; } } clear ( this . containingProjects ) ; } getDefaultProject ( ) { switch ( this . containingProjects . length ) { case 0 : return Errors . ThrowNoProject ( ) ; case 1 : return ensureNotAutoImportProvider ( this . containingProjects [ 0 ] ) ; default : let firstExternalProject ; let firstConfiguredProject ; let firstInferredProject ; let firstNonSourceOfProjectReferenceRedirect ; let defaultConfiguredProject ; for ( let index = 0 ; index < this . containingProjects . length ; index ++ ) { const project = this . containingProjects [ index ] ; if ( isConfiguredProject ( project ) ) { if ( ! project . isSourceOfProjectReferenceRedirect ( this . fileName ) ) { if ( defaultConfiguredProject === undefined && index !== this . containingProjects . length - 1 ) { defaultConfiguredProject = project . projectService . findDefaultConfiguredProject ( this ) || false ; } if ( defaultConfiguredProject === project ) return project ; if ( ! firstNonSourceOfProjectReferenceRedirect ) firstNonSourceOfProjectReferenceRedirect = project ; } if ( ! firstConfiguredProject ) firstConfiguredProject = project ; } else if ( ! firstExternalProject && isExternalProject ( project ) ) { firstExternalProject = project ; } else if ( ! firstInferredProject && isInferredProject ( project ) ) { firstInferredProject = project ; } } return ensureNotAutoImportProvider ( defaultConfiguredProject || firstNonSourceOfProjectReferenceRedirect || firstConfiguredProject || firstExternalProject || firstInferredProject ) ; } } registerFileUpdate ( ) { for ( const p of this . containingProjects ) { p . registerFileUpdate ( this . path ) ; } } setOptions ( formatSettings , preferences ) { if ( formatSettings ) { if ( ! this . formatSettings ) { this . formatSettings = getDefaultFormatCodeSettings ( this . host . newLine ) ; assign ( this . formatSettings , formatSettings ) ; } else { this . formatSettings = { ... this . formatSettings , ... formatSettings } ; } } if ( preferences ) { if ( ! this . preferences ) { this . preferences = emptyOptions ; } this . preferences = { ... this . preferences , ... preferences } ; } } getLatestVersion ( ) { this . textStorage . getSnapshot ( ) ; return this . textStorage . getVersion ( ) ; } saveTo ( fileName ) { this . host . writeFile ( fileName , getSnapshotText ( this . textStorage . getSnapshot ( ) ) ) ; } delayReloadNonMixedContentFile ( ) { Debug . assert ( ! this . isDynamicOrHasMixedContent ( ) ) ; this . textStorage . delayReloadFromFileIntoText ( ) ; this . markContainingProjectsAsDirty ( ) ; } reloadFromFile ( tempFileName ? ) { if ( this . isDynamicOrHasMixedContent ( ) ) { this . textStorage . reload ( "" ) ; this . markContainingProjectsAsDirty ( ) ; return true ; } else { if ( this . textStorage . reloadWithFileText ( tempFileName ) ) { this . markContainingProjectsAsDirty ( ) ; return true ; } } return false ; } getAbsolutePositionAndLineText ( line ) { return this . textStorage . getAbsolutePositionAndLineText ( line ) ; } editContent ( start , end , newText ) { this . textStorage . edit ( start , end , newText ) ; this . markContainingProjectsAsDirty ( ) ; } markContainingProjectsAsDirty ( ) { for ( const p of this . containingProjects ) { p . markFileAsDirty ( this . path ) ; } } isOrphan ( ) { return ! forEach ( this . containingProjects , p => ! p . isOrphan ( ) ) ; } isContainedByAutoImportProvider ( ) { return some ( this . containingProjects , p => p . projectKind === ProjectKind . AutoImportProvider ) ; } param line lineToTextSpan ( line ) { return this . textStorage . lineToTextSpan ( line ) ; } param line param offset lineOffsetToPosition ( line , offset ) ; lineOffsetToPosition ( line , offset , allowEdits ? ) ; lineOffsetToPosition ( line , offset , allowEdits ? ) { return this . textStorage . lineOffsetToPosition ( line , offset , allowEdits ) ; } positionToLineOffset ( position ) { failIfInvalidPosition ( position ) ; const location = this . textStorage . positionToLineOffset ( position ) ; failIfInvalidLocation ( location ) ; return location ; } public isJavaScript ( ) { return this . scriptKind === ScriptKind . JS || this . scriptKind === ScriptKind . JSX ; } getLineInfo ( ) { return this . textStorage . getLineInfo ( ) ; } closeSourceMapFileWatcher ( ) { if ( this . sourceMapFilePath && ! isString ( this . sourceMapFilePath ) ) { closeFileWatcherOf ( this . sourceMapFilePath ) ; this . sourceMapFilePath = undefined ; } } } function ensureNotAutoImportProvider ( project ) { if ( ! project || project . projectKind === ProjectKind . AutoImportProvider ) { return Errors . ThrowNoProject ( ) ; } return project ; } function failIfInvalidPosition ( position ) { Debug . assert ( typeof position === "number" , `Expected position ${ position } to be a number.` ) ; Debug . assert ( position >= 0 , `Expected position to be non-negative.` ) ; } function failIfInvalidLocation ( location ) { Debug . assert ( typeof location . line === "number" , `Expected line ${ location . line } to be a number.` ) ; Debug . assert ( typeof location . offset === "number" , `Expected offset ${ location . offset } to be a number.` ) ; Debug . assert ( location . line > 0 , `Expected line to be non-${ location . line === 0 ? "zero" : "negative" }` ) ; Debug . assert ( location . offset > 0 , `Expected offset to be non-${ location . offset === 0 ? "zero" : "negative" }` ) ; } }