Add method on host to get DocumentPositionMapper so it can be cached.

This commit is contained in:
Sheetal Nandi 2018-11-16 10:15:51 -08:00
parent 0dad79e8b3
commit 12428d45c0
13 changed files with 219 additions and 89 deletions

View File

@ -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 */

View File

@ -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<number>): 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<number> = 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];

View File

@ -2614,6 +2614,8 @@ namespace ts {
export interface SourceFileLike {
readonly text: string;
lineMap?: ReadonlyArray<number>;
/* @internal */
getPositionOfLineAndCharacter?(line: number, character: number): number;
}

View File

@ -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) {

View File

@ -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();
}

View File

@ -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 {

View File

@ -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 };

View File

@ -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;

View File

@ -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
});

View File

@ -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<SourceFileLike | false>();
const documentPositionMappers = createMap<DocumentPositionMapper>();
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);
}
};
}
}

View File

@ -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<ApplyCodeActionCommandResult>;
/* @internal */ inspectValue?(options: InspectValueOptions): Promise<ValueInfo>;
writeFile?(fileName: string, content: string): void;
/* @internal */
getDocumentPositionMapper?(fileName: string): DocumentPositionMapper | undefined;
/* @internal */
getSourceFileLike?(fileName: string): SourceFileLike | undefined;
}
/* @internal */

View File

@ -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");
});

View File

@ -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<Diagnostic>;
getAllProjectErrors(): ReadonlyArray<Diagnostic>;
getLanguageService(ensureSynchronized?: boolean): LanguageService;
private readMapFile;
private shouldEmitFile;
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[];
/**