diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index a9438110593..e9ccc23d6eb 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -338,12 +338,16 @@ namespace ts { } export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number { - return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text); + return sourceFile.getPositionOfLineAndCharacter ? + sourceFile.getPositionOfLineAndCharacter(line, character) : + computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text); } /* @internal */ export function getPositionOfLineAndCharacterWithEdits(sourceFile: SourceFileLike, line: number, character: number): number { - return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, /*allowEdits*/ true); + return sourceFile.getPositionOfLineAndCharacter ? + sourceFile.getPositionOfLineAndCharacter(line, character) : + computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, /*allowEdits*/ true); } /* @internal */ diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index b3b4e1fbb5e..190cac3b761 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -266,14 +266,24 @@ namespace ts { const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\s*$/; const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; + export interface LineInfo { + getLineCount(): number; + getLineText(line: number): string; + } + + export function getLineInfo(text: string, lineStarts: ReadonlyArray): LineInfo { + return { + getLineCount: () => lineStarts.length, + getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1]) + }; + } + /** * Tries to find the sourceMappingURL comment at the end of a file. - * @param text The source text of the file. - * @param lineStarts The line starts of the file. */ - export function tryGetSourceMappingURL(text: string, lineStarts: ReadonlyArray = computeLineStarts(text)) { - for (let index = lineStarts.length - 1; index >= 0; index--) { - const line = text.substring(lineStarts[index], lineStarts[index + 1]); + export function tryGetSourceMappingURL(lineInfo: LineInfo) { + for (let index = lineInfo.getLineCount() - 1; index >= 0; index--) { + const line = lineInfo.getLineText(index); const comment = sourceMapCommentRegExp.exec(line); if (comment) { return comment[1]; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4da96730a93..e654f4faab7 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2614,6 +2614,8 @@ namespace ts { export interface SourceFileLike { readonly text: string; lineMap?: ReadonlyArray; + /* @internal */ + getPositionOfLineAndCharacter?(line: number, character: number): number; } diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts index 92c3e2d4a94..d16aca21d66 100644 --- a/src/harness/sourceMapRecorder.ts +++ b/src/harness/sourceMapRecorder.ts @@ -69,7 +69,7 @@ namespace Harness.SourceMapRecorder { SourceMapDecoder.initializeSourceMapDecoding(sourceMapData); sourceMapRecorder.WriteLine("==================================================================="); sourceMapRecorder.WriteLine("JsFile: " + sourceMapData.sourceMap.file); - sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(jsFile.text, jsLineMap)); + sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(ts.getLineInfo(jsFile.text, jsLineMap))); sourceMapRecorder.WriteLine("sourceRoot: " + sourceMapData.sourceMap.sourceRoot); sourceMapRecorder.WriteLine("sources: " + sourceMapData.sourceMap.sources); if (sourceMapData.sourceMap.sourcesContent) { diff --git a/src/server/project.ts b/src/server/project.ts index fa39aa4abe9..566a2b9abe9 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -503,6 +503,66 @@ namespace ts.server { return this.getLanguageService().getSourceMapper(); } + /*@internal*/ + getDocumentPositionMapper(fileName: string): DocumentPositionMapper | undefined { + const declarationInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost); + if (!declarationInfo) return undefined; + + declarationInfo.getSnapshot(); // Ensure synchronized + const existingMapper = declarationInfo.textStorage.mapper; + if (existingMapper !== undefined) { + return existingMapper ? existingMapper : undefined; + } + + // Create the mapper + declarationInfo.mapInfo = undefined; + + const mapper = getDocumentPositionMapper({ + getCanonicalFileName: this.projectService.toCanonicalFileName, + log: s => this.log(s), + readMapFile: f => this.readMapFile(f, declarationInfo), + getSourceFileLike: f => this.getSourceFileLike(f) + }, declarationInfo.fileName, declarationInfo.textStorage.getLineInfo()); + declarationInfo.textStorage.mapper = mapper || false; + return mapper; + } + + private readMapFile(fileName: string, declarationInfo: ScriptInfo) { + const mapInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost); + if (!mapInfo) return undefined; + declarationInfo.mapInfo = mapInfo; + const snap = mapInfo.getSnapshot(); + return snap.getText(0, snap.getLength()); + } + + /*@internal*/ + getSourceFileLike(fileName: string) { + const path = this.toPath(fileName); + const sourceFile = this.getSourceFile(path); + if (sourceFile && sourceFile.resolvedPath === path) return sourceFile; + + // Need to look for other files. + const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost); + if (!info) return undefined; + + // Key doesnt matter since its only for text and lines + if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile; + if (info.textStorage.sourceFileLike) return info.textStorage.sourceFileLike; + + info.textStorage.sourceFileLike = { + get text() { + Debug.fail("shouldnt need text"); + return ""; + }, + getLineAndCharacterOfPosition: pos => { + const lineOffset = info.positionToLineOffset(pos); + return { line: lineOffset.line - 1, character: lineOffset.offset - 1 }; + }, + getPositionOfLineAndCharacter: (line, character) => info.lineOffsetToPosition(line + 1, character + 1) + }; + return info.textStorage.sourceFileLike; + } + private shouldEmitFile(scriptInfo: ScriptInfo) { return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index c090b4b14d0..3502fff1556 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -46,6 +46,9 @@ namespace ts.server { */ private pendingReloadFromDisk = false; + mapper: DocumentPositionMapper | false | undefined = false; + sourceFileLike: SourceFileLike | undefined; + constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath, initialVersion: ScriptInfoVersion | undefined, private readonly info: ScriptInfo) { this.version = initialVersion || { svc: 0, text: 0 }; } @@ -70,6 +73,8 @@ namespace ts.server { this.text = newText; this.lineMap = undefined; this.fileSize = undefined; + this.mapper = undefined; + this.sourceFileLike = undefined; this.version.text++; } @@ -79,6 +84,8 @@ namespace ts.server { this.text = undefined; this.lineMap = undefined; this.fileSize = undefined; + this.mapper = undefined; + this.sourceFileLike = undefined; } /** @@ -156,8 +163,8 @@ namespace ts.server { : ScriptSnapshot.fromString(this.getOrLoadText()); } - public getLineInfo(line: number): AbsolutePositionAndLineText { - return this.switchToScriptVersionCache().getLineInfo(line); + public getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { + return this.switchToScriptVersionCache().getAbsolutePositionAndLineText(line); } /** * @param line 0 based index @@ -246,6 +253,17 @@ namespace ts.server { Debug.assert(!this.svc, "ScriptVersionCache should not be set"); return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText())); } + + getLineInfo(): LineInfo { + 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); + } } /*@internal*/ @@ -269,7 +287,7 @@ namespace ts.server { /* @internal */ fileWatcher: FileWatcher | undefined; - private textStorage: TextStorage; + /* @internal */ textStorage: TextStorage; /*@internal*/ readonly isDynamic: boolean; @@ -284,6 +302,9 @@ namespace ts.server { /*@internal*/ mTime?: number; + /*@internal*/ + mapInfo?: ScriptInfo; + constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, @@ -521,8 +542,8 @@ namespace ts.server { } /*@internal*/ - getLineInfo(line: number): AbsolutePositionAndLineText { - return this.textStorage.getLineInfo(line); + getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { + return this.textStorage.getAbsolutePositionAndLineText(line); } editContent(start: number, end: number, newText: string): void { diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index a350ed99f71..afd8f67ecbe 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -308,8 +308,8 @@ namespace ts.server { return this._getSnapshot().version; } - getLineInfo(line: number): AbsolutePositionAndLineText { - return this._getSnapshot().index.lineNumberToInfo(line); + getAbsolutePositionAndLineText(oneBasedLine: number): AbsolutePositionAndLineText { + return this._getSnapshot().index.lineNumberToInfo(oneBasedLine); } lineOffsetToPosition(line: number, column: number): number { @@ -348,6 +348,10 @@ namespace ts.server { } } + getLineCount() { + return this._getSnapshot().index.getLineCount(); + } + static fromString(script: string) { const svc = new ScriptVersionCache(); const snap = new LineIndexSnapshot(0, svc, new LineIndex()); @@ -400,8 +404,12 @@ namespace ts.server { return this.root.charOffsetToLineInfo(1, position); } + getLineCount() { + return this.root.lineCount(); + } + lineNumberToInfo(oneBasedLine: number): AbsolutePositionAndLineText { - const lineCount = this.root.lineCount(); + const lineCount = this.getLineCount(); if (oneBasedLine <= lineCount) { const { position, leaf } = this.root.lineNumberToInfo(oneBasedLine, 0); return { absolutePosition: position, lineText: leaf && leaf.text }; diff --git a/src/server/session.ts b/src/server/session.ts index 0b9ad913f8a..d534f445ee3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1474,7 +1474,7 @@ namespace ts.server { // only to the previous line. If all this is true, then // add edits necessary to properly indent the current line. if ((args.key === "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { - const { lineText, absolutePosition } = scriptInfo.getLineInfo(args.line); + const { lineText, absolutePosition } = scriptInfo.getAbsolutePositionAndLineText(args.line); if (lineText && lineText.search("\\S") < 0) { const preferredIndent = languageService.getIndentationAtPosition(file, position, formatOptions); let hasIndent = 0; diff --git a/src/services/services.ts b/src/services/services.ts index 67f9e537574..a0c41ffe94d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -603,7 +603,7 @@ namespace ts { } public getPositionOfLineAndCharacter(line: number, character: number): number { - return getPositionOfLineAndCharacter(this, line, character); + return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text); } public getLineEndOfPosition(pos: number): number { @@ -1143,8 +1143,10 @@ namespace ts { useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getCurrentDirectory: () => currentDirectory, getProgram, - fileExists: host.fileExists ? f => host.fileExists!(f) : returnFalse, - readFile: host.readFile ? (f, encoding) => host.readFile!(f, encoding) : () => undefined, + fileExists: host.fileExists && (f => host.fileExists!(f)), + readFile: host.readFile && ((f, encoding) => host.readFile!(f, encoding)), + getDocumentPositionMapper: host.getDocumentPositionMapper && (f => host.getDocumentPositionMapper!(f)), + getSourceFileLike: host.getSourceFileLike && (f => host.getSourceFileLike!(f)), log }); diff --git a/src/services/sourcemaps.ts b/src/services/sourcemaps.ts index 7b51c9ecff9..c679688e6c0 100644 --- a/src/services/sourcemaps.ts +++ b/src/services/sourcemaps.ts @@ -13,8 +13,10 @@ namespace ts { useCaseSensitiveFileNames(): boolean; getCurrentDirectory(): string; getProgram(): Program | undefined; - fileExists(path: string): boolean; - readFile(path: string, encoding?: string): string | undefined; + fileExists?(path: string): boolean; + readFile?(path: string, encoding?: string): string | undefined; + getSourceFileLike?(fileName: string): SourceFileLike | undefined; + getDocumentPositionMapper?(fileName: string): DocumentPositionMapper | undefined; log(s: string): void; } @@ -22,63 +24,33 @@ namespace ts { const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); const currentDirectory = host.getCurrentDirectory(); const sourceFileLike = createMap(); + const documentPositionMappers = createMap(); return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; function toPath(fileName: string) { return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } - function scanForSourcemapURL(fileName: string) { - const mappedFile = sourceFileLike.get(toPath(fileName)); - if (!mappedFile) { - return; - } + function getDocumentPositionMapper(fileName: string) { + const path = toPath(fileName); + const value = documentPositionMappers.get(path); + if (value) return value; - return tryGetSourceMappingURL(mappedFile.text, getLineStarts(mappedFile)); - } - - function convertDocumentToSourceMapper(file: SourceFileLike, contents: string, mapFileName: string) { - const map = tryParseRawSourceMap(contents); - if (!map || !map.sources || !map.file || !map.mappings) { - // obviously invalid map - return file.sourceMapper = identitySourceMapConsumer; + let mapper: DocumentPositionMapper | undefined; + if (host.getDocumentPositionMapper) { + mapper = host.getDocumentPositionMapper(fileName); } - - return file.sourceMapper = createDocumentPositionMapper({ - getSourceFileLike, - getCanonicalFileName, - log: s => host.log(s), - }, map, mapFileName); - } - - function getSourceMapper(fileName: string, file: SourceFileLike): DocumentPositionMapper { - if (file.sourceMapper) { - return file.sourceMapper; + else if (host.readFile) { + const file = getSourceFileLike(fileName); + mapper = file && ts.getDocumentPositionMapper({ + getSourceFileLike, + getCanonicalFileName, + log: s => host.log(s), + readMapFile: f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined + }, fileName, getLineInfo(file.text, getLineStarts(file))); } - let mapFileName = scanForSourcemapURL(fileName); - if (mapFileName) { - const match = base64UrlRegExp.exec(mapFileName); - if (match) { - if (match[1]) { - const base64Object = match[1]; - return convertDocumentToSourceMapper(file, base64decode(sys, base64Object), fileName); - } - // Not a data URL we can parse, skip it - mapFileName = undefined; - } - } - const possibleMapLocations: string[] = []; - if (mapFileName) { - possibleMapLocations.push(mapFileName); - } - possibleMapLocations.push(fileName + ".map"); - for (const location of possibleMapLocations) { - const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(fileName)); - if (host.fileExists(mapFileName)) { - return convertDocumentToSourceMapper(file, host.readFile(mapFileName)!, mapFileName); // TODO: GH#18217 - } - } - return file.sourceMapper = identitySourceMapConsumer; + documentPositionMappers.set(path, mapper || identitySourceMapConsumer); + return mapper || identitySourceMapConsumer; } function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined { @@ -87,8 +59,8 @@ namespace ts { const file = getSourceFile(info.fileName); if (!file) return undefined; - const newLoc = getSourceMapper(info.fileName, file).getSourcePosition(info); - return newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; + const newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); + return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; } function tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined { @@ -106,10 +78,7 @@ namespace ts { getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); if (declarationPath === undefined) return undefined; - const declarationFile = getSourceFileLikeFromCache(declarationPath); - if (!declarationFile) return undefined; - - const newLoc = getSourceMapper(declarationPath, declarationFile).getGeneratedPosition(info); + const newLoc = getDocumentPositionMapper(declarationPath).getGeneratedPosition(info); return newLoc === info ? undefined : newLoc; } @@ -123,42 +92,92 @@ namespace ts { return file && file.resolvedPath === path ? file : undefined; } - function getSourceFileLikeFromCache(fileName: string): SourceFileLike | undefined { + function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined { const path = toPath(fileName); const fileFromCache = sourceFileLike.get(path); if (fileFromCache !== undefined) return fileFromCache ? fileFromCache : undefined; // TODO: should ask host instead? - if (!host.fileExists(path)) { + if (!host.readFile || host.fileExists && !host.fileExists(path)) { sourceFileLike.set(path, false); return undefined; } // And failing that, check the disk const text = host.readFile(path); - const file: SourceFileLike | false = text ? { - text, - lineMap: undefined, - getLineAndCharacterOfPosition(pos: number) { - return computeLineAndCharacterOfPosition(getLineStarts(this as SourceFileLike), pos); - } - } : false; + const file = text ? createSourceFileLike(text) : false; sourceFileLike.set(path, file); return file ? file : undefined; } // This can be called from source mapper in either source program or program that includes generated file function getSourceFileLike(fileName: string) { - return getSourceFile(fileName) || getSourceFileLikeFromCache(fileName); + return !host.getSourceFileLike ? + getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) : + host.getSourceFileLike(fileName); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { + // TODO:: shkamat const file = getSourceFileLike(fileName)!; // TODO: GH#18217 return file.getLineAndCharacterOfPosition(position); } function clearCache(): void { sourceFileLike.clear(); + documentPositionMappers.clear(); } } + + export interface GetDocumentPositionMapperHost extends DocumentPositionMapperHost { + readMapFile(fileName: string): string | undefined; + } + + export function getDocumentPositionMapper(host: GetDocumentPositionMapperHost, generatedFileName: string, generatedFileLineInfo: LineInfo) { + let mapFileName = tryGetSourceMappingURL(generatedFileLineInfo); + if (mapFileName) { + const match = base64UrlRegExp.exec(mapFileName); + if (match) { + if (match[1]) { + const base64Object = match[1]; + return convertDocumentToSourceMapper(host, base64decode(sys, base64Object), generatedFileName); + } + // Not a data URL we can parse, skip it + mapFileName = undefined; + } + } + const possibleMapLocations: string[] = []; + if (mapFileName) { + possibleMapLocations.push(mapFileName); + } + possibleMapLocations.push(generatedFileName + ".map"); + for (const location of possibleMapLocations) { + const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName)); + const mapFileContents = host.readMapFile(mapFileName); + if (mapFileContents) { + return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); + } + } + return undefined; + } + + function convertDocumentToSourceMapper(host: DocumentPositionMapperHost, contents: string, mapFileName: string) { + const map = tryParseRawSourceMap(contents); + if (!map || !map.sources || !map.file || !map.mappings) { + // obviously invalid map + return undefined; + } + + return createDocumentPositionMapper(host, map, mapFileName); + } + + function createSourceFileLike(text: string, lineMap?: SourceFileLike["lineMap"]): SourceFileLike { + return { + text, + lineMap, + getLineAndCharacterOfPosition(pos: number) { + return computeLineAndCharacterOfPosition(getLineStarts(this), pos); + } + }; + } } diff --git a/src/services/types.ts b/src/services/types.ts index 69f07b705a7..d3a8f8cdd52 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -91,7 +91,6 @@ namespace ts { export interface SourceFileLike { getLineAndCharacterOfPosition(pos: number): LineAndCharacter; - /*@internal*/ sourceMapper?: DocumentPositionMapper; } export interface SourceMapSource { @@ -233,6 +232,11 @@ namespace ts { installPackage?(options: InstallPackageOptions): Promise; /* @internal */ inspectValue?(options: InspectValueOptions): Promise; writeFile?(fileName: string, content: string): void; + + /* @internal */ + getDocumentPositionMapper?(fileName: string): DocumentPositionMapper | undefined; + /* @internal */ + getSourceFileLike?(fileName: string): SourceFileLike | undefined; } /* @internal */ diff --git a/src/testRunner/unittests/textStorage.ts b/src/testRunner/unittests/textStorage.ts index 5dce083120b..f5fbb512e52 100644 --- a/src/testRunner/unittests/textStorage.ts +++ b/src/testRunner/unittests/textStorage.ts @@ -60,7 +60,7 @@ namespace ts.textStorage { ts1.useText(); assert.isFalse(ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 2"); - ts1.getLineInfo(0); + ts1.getAbsolutePositionAndLineText(0); assert.isTrue(ts1.hasScriptVersionCache_TestOnly(), "have script version cache - 2"); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 2b78178587e..b788d46ac9f 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8046,7 +8046,6 @@ declare namespace ts.server { readonly containingProjects: Project[]; private formatSettings; private preferences; - private textStorage; constructor(host: ServerHost, fileName: NormalizedPath, scriptKind: ScriptKind, hasMixedContent: boolean, path: Path, initialVersion?: ScriptInfoVersion); isScriptOpen(): boolean; open(newText: string): void; @@ -8211,6 +8210,7 @@ declare namespace ts.server { getGlobalProjectErrors(): ReadonlyArray; getAllProjectErrors(): ReadonlyArray; getLanguageService(ensureSynchronized?: boolean): LanguageService; + private readMapFile; private shouldEmitFile; getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /**