Merge pull request #28886 from Microsoft/sourceMapDecoder

Enhancements to SourceMap decoder from tsserver
This commit is contained in:
Sheetal Nandi
2018-12-19 12:51:42 -08:00
committed by GitHub
17 changed files with 1102 additions and 202 deletions

View File

@@ -787,7 +787,13 @@ namespace ts {
// Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it.
let redirectTargetsMap = createMultiMap<string>();
const filesByName = createMap<SourceFile | undefined>();
/**
* map with
* - SourceFile if present
* - false if sourceFile missing for source of project reference redirect
* - undefined otherwise
*/
const filesByName = createMap<SourceFile | false | undefined>();
let missingFilePaths: ReadonlyArray<Path> | undefined;
// stores 'filename -> file association' ignoring case
// used to track cases when two file names differ only in casing
@@ -854,7 +860,7 @@ namespace ts {
}
}
missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined));
files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles);
processingDefaultLibFiles = undefined;
processingOtherFiles = undefined;
@@ -1567,7 +1573,7 @@ namespace ts {
}
function getSourceFileByPath(path: Path): SourceFile | undefined {
return filesByName.get(path);
return filesByName.get(path) || undefined;
}
function getDiagnosticsHelper<T extends Diagnostic>(
@@ -2104,7 +2110,7 @@ namespace ts {
/** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */
function getSourceFileFromReference(referencingFile: SourceFile, ref: FileReference): SourceFile | undefined {
return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)));
return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)) || undefined);
}
function getSourceFileFromReferenceWorker(
@@ -2235,7 +2241,7 @@ namespace ts {
}
}
return file;
return file || undefined;
}
let redirectedPath: Path | undefined;
@@ -2327,9 +2333,12 @@ namespace ts {
}
function addFileToFilesByName(file: SourceFile | undefined, path: Path, redirectedPath: Path | undefined) {
filesByName.set(path, file);
if (redirectedPath) {
filesByName.set(redirectedPath, file);
filesByName.set(path, file || false);
}
else {
filesByName.set(path, file);
}
}

View File

@@ -337,13 +337,14 @@ namespace ts {
return result;
}
export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number {
return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text);
}
export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number;
/* @internal */
export function getPositionOfLineAndCharacterWithEdits(sourceFile: SourceFileLike, line: number, character: number): number {
return computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, /*allowEdits*/ true);
// tslint:disable-next-line:unified-signatures
export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number;
export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number {
return sourceFile.getPositionOfLineAndCharacter ?
sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) :
computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits);
}
/* @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];
@@ -573,7 +583,10 @@ namespace ts {
}
function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) {
return compareValues(left.sourceIndex, right.sourceIndex);
// Compares sourcePosition without comparing sourceIndex
// since the mappings are grouped by sourceIndex
Debug.assert(left.sourceIndex === right.sourceIndex);
return compareValues(left.sourcePosition, right.sourcePosition);
}
function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) {
@@ -592,11 +605,9 @@ namespace ts {
const mapDirectory = getDirectoryPath(mapPath);
const sourceRoot = map.sourceRoot ? getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory;
const generatedAbsoluteFilePath = getNormalizedAbsolutePath(map.file, mapDirectory);
const generatedCanonicalFilePath = host.getCanonicalFileName(generatedAbsoluteFilePath) as Path;
const generatedFile = host.getSourceFileLike(generatedCanonicalFilePath);
const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath);
const sourceFileAbsolutePaths = map.sources.map(source => getNormalizedAbsolutePath(source, sourceRoot));
const sourceFileCanonicalPaths = sourceFileAbsolutePaths.map(source => host.getCanonicalFileName(source) as Path);
const sourceToSourceIndexMap = createMapFromEntries(sourceFileCanonicalPaths.map((source, i) => [source, i] as [string, number]));
const sourceToSourceIndexMap = createMapFromEntries(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i] as [string, number]));
let decodedMappings: ReadonlyArray<MappedPosition> | undefined;
let generatedMappings: SortedReadonlyArray<MappedPosition> | undefined;
let sourceMappings: ReadonlyArray<SortedReadonlyArray<SourceMappedPosition>> | undefined;
@@ -608,16 +619,15 @@ namespace ts {
function processMapping(mapping: Mapping): MappedPosition {
const generatedPosition = generatedFile !== undefined
? getPositionOfLineAndCharacterWithEdits(generatedFile, mapping.generatedLine, mapping.generatedCharacter)
? getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true)
: -1;
let source: string | undefined;
let sourcePosition: number | undefined;
if (isSourceMapping(mapping)) {
const sourceFilePath = sourceFileCanonicalPaths[mapping.sourceIndex];
const sourceFile = host.getSourceFileLike(sourceFilePath);
const sourceFile = host.getSourceFileLike(sourceFileAbsolutePaths[mapping.sourceIndex]);
source = map.sources[mapping.sourceIndex];
sourcePosition = sourceFile !== undefined
? getPositionOfLineAndCharacterWithEdits(sourceFile, mapping.sourceLine, mapping.sourceCharacter)
? getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true)
: -1;
}
return {

View File

@@ -2617,6 +2617,8 @@ namespace ts {
export interface SourceFileLike {
readonly text: string;
lineMap?: ReadonlyArray<number>;
/* @internal */
getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number;
}
@@ -5531,9 +5533,9 @@ namespace ts {
/* @internal */
export interface DocumentPositionMapperHost {
getSourceFileLike(path: Path): SourceFileLike | undefined;
getSourceFileLike(fileName: string): SourceFileLike | undefined;
getCanonicalFileName(path: string): string;
log?(text: string): void;
log(text: string): void;
}
/**

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

@@ -342,6 +342,7 @@ namespace ts.server {
FailedLookupLocation = "Directory of Failed lookup locations in module resolution",
TypeRoots = "Type root directory",
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
MissingSourceMapFile = "Missing source map file"
}
const enum ConfigFileWatcherStatus {
@@ -437,7 +438,8 @@ namespace ts.server {
/**
* Container of all known scripts
*/
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
/*@internal*/
readonly filenameToScriptInfo = createMap<ScriptInfo>();
private readonly scriptInfoInNodeModulesWatchers = createMap <ScriptInfoInNodeModulesWatcher>();
/**
* Contains all the deleted script info's version information so that
@@ -944,10 +946,42 @@ namespace ts.server {
// this file and set of inferred projects
info.delayReloadNonMixedContentFile();
this.delayUpdateProjectGraphs(info.containingProjects);
this.handleSourceMapProjects(info);
}
}
}
private handleSourceMapProjects(info: ScriptInfo) {
// Change in d.ts, update source projects as well
if (info.sourceMapFilePath) {
if (isString(info.sourceMapFilePath)) {
const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos);
}
else {
this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos);
}
}
// Change in mapInfo, update declarationProjects and source projects
this.delayUpdateSourceInfoProjects(info.sourceInfos);
if (info.declarationInfoPath) {
this.delayUpdateProjectsOfScriptInfoPath(info.declarationInfoPath);
}
}
private delayUpdateSourceInfoProjects(sourceInfos: Map<true> | undefined) {
if (sourceInfos) {
sourceInfos.forEach((_value, path) => this.delayUpdateProjectsOfScriptInfoPath(path as Path));
}
}
private delayUpdateProjectsOfScriptInfoPath(path: Path) {
const info = this.getScriptInfoForPath(path);
if (info) {
this.delayUpdateProjectGraphs(info.containingProjects);
}
}
private handleDeletedFile(info: ScriptInfo) {
this.stopWatchingScriptInfo(info);
@@ -961,6 +995,15 @@ namespace ts.server {
// update projects to make sure that set of referenced files is correct
this.delayUpdateProjectGraphs(containingProjects);
this.handleSourceMapProjects(info);
info.closeSourceMapFileWatcher();
// need to recalculate source map from declaration file
if (info.declarationInfoPath) {
const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath);
if (declarationInfo) {
declarationInfo.sourceMapFilePath = undefined;
}
}
}
}
@@ -2195,6 +2238,150 @@ namespace ts.server {
return this.filenameToScriptInfo.get(fileName);
}
/*@internal*/
getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
// Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host
const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host);
if (!declarationInfo) return undefined;
// Try to get from cache
declarationInfo.getSnapshot(); // Ensure synchronized
if (isString(declarationInfo.sourceMapFilePath)) {
// Ensure mapper is synchronized
const sourceMapFileInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath);
if (sourceMapFileInfo) {
sourceMapFileInfo.getSnapshot();
if (sourceMapFileInfo.documentPositionMapper !== undefined) {
sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
return sourceMapFileInfo.documentPositionMapper ? sourceMapFileInfo.documentPositionMapper : undefined;
}
}
declarationInfo.sourceMapFilePath = undefined;
}
else if (declarationInfo.sourceMapFilePath) {
declarationInfo.sourceMapFilePath.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, declarationInfo.sourceMapFilePath.sourceInfos);
return undefined;
}
else if (declarationInfo.sourceMapFilePath !== undefined) {
// Doesnt have sourceMap
return undefined;
}
// Create the mapper
let sourceMapFileInfo: ScriptInfo | undefined;
let mapFileNameFromDeclarationInfo: string | undefined;
let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => {
const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host);
if (!mapInfo) {
mapFileNameFromDeclarationInfo = mapFileNameFromDts;
return undefined;
}
sourceMapFileInfo = mapInfo;
const snap = mapInfo.getSnapshot();
if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper;
return snap.getText(0, snap.getLength());
};
const projectName = project.projectName;
const documentPositionMapper = getDocumentPositionMapper(
{ getCanonicalFileName: this.toCanonicalFileName, log: s => this.logger.info(s), getSourceFileLike: f => this.getSourceFileLike(f, projectName, declarationInfo) },
declarationInfo.fileName,
declarationInfo.getLineInfo(),
readMapFile
);
readMapFile = undefined; // Remove ref to project
if (sourceMapFileInfo) {
declarationInfo.sourceMapFilePath = sourceMapFileInfo.path;
sourceMapFileInfo.declarationInfoPath = declarationInfo.path;
sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false;
sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
}
else if (mapFileNameFromDeclarationInfo) {
declarationInfo.sourceMapFilePath = {
watcher: this.addMissingSourceMapFile(
project.currentDirectory === this.currentDirectory ?
mapFileNameFromDeclarationInfo :
getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory),
declarationInfo.path
),
sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project)
};
}
else {
declarationInfo.sourceMapFilePath = false;
}
return documentPositionMapper;
}
private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Map<true>) {
if (sourceFileName) {
// Attach as source
const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!;
(sourceInfos || (sourceInfos = createMap())).set(sourceInfo.path, true);
}
return sourceInfos;
}
private addMissingSourceMapFile(mapFileName: string, declarationInfoPath: Path) {
const fileWatcher = this.watchFactory.watchFile(
this.host,
mapFileName,
() => {
const declarationInfo = this.getScriptInfoForPath(declarationInfoPath);
if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) {
// Update declaration and source projects
this.delayUpdateProjectGraphs(declarationInfo.containingProjects);
this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos);
declarationInfo.closeSourceMapFileWatcher();
}
},
PollingInterval.High,
WatchType.MissingSourceMapFile,
);
return fileWatcher;
}
/*@internal*/
getSourceFileLike(fileName: string, projectNameOrProject: string | Project, declarationInfo?: ScriptInfo) {
const project = (projectNameOrProject as Project).projectName ? projectNameOrProject as Project : this.findProject(projectNameOrProject as string);
if (project) {
const path = project.toPath(fileName);
const sourceFile = project.getSourceFile(path);
if (sourceFile && sourceFile.resolvedPath === path) return sourceFile;
}
// Need to look for other files.
const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host);
if (!info) return undefined;
// Attach as source
if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) {
const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath);
if (sourceMapInfo) {
(sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = createMap())).set(info.path, true);
}
}
// Key doesnt matter since its only for text and lines
if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile;
// Create sourceFileLike
if (!info.sourceFileLike) {
info.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, allowEdits) => info.lineOffsetToPosition(line + 1, character + 1, allowEdits)
};
}
return info.sourceFileLike;
}
setHostConfiguration(args: protocol.ConfigureRequestArguments) {
if (args.file) {
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file));
@@ -2416,7 +2603,7 @@ namespace ts.server {
/** @internal */
fileExists(fileName: NormalizedPath): boolean {
return this.filenameToScriptInfo.has(fileName) || this.host.fileExists(fileName);
return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName);
}
private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined {
@@ -2490,13 +2677,7 @@ namespace ts.server {
// when some file/s were closed which resulted in project removal.
// It was then postponed to cleanup these script infos so that they can be reused if
// the file from that old project is reopened because of opening file from here.
this.filenameToScriptInfo.forEach(info => {
if (!info.isScriptOpen() && info.isOrphan()) {
// if there are not projects that include this script info - delete it
this.stopWatchingScriptInfo(info);
this.deleteScriptInfo(info);
}
});
this.removeOrphanScriptInfos();
this.printProjects();
@@ -2539,6 +2720,57 @@ namespace ts.server {
}
}
private removeOrphanScriptInfos() {
const toRemoveScriptInfos = cloneMap(this.filenameToScriptInfo);
this.filenameToScriptInfo.forEach(info => {
// If script info is open or orphan, retain it and its dependencies
if (!info.isScriptOpen() && info.isOrphan()) {
// Otherwise if there is any source info that is alive, this alive too
if (!info.sourceMapFilePath) return;
let sourceInfos: Map<true> | undefined;
if (isString(info.sourceMapFilePath)) {
const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
}
else {
sourceInfos = info.sourceMapFilePath.sourceInfos;
}
if (!sourceInfos) return;
if (!forEachKey(sourceInfos, path => {
const info = this.getScriptInfoForPath(path as Path);
return !!info && (info.isScriptOpen() || !info.isOrphan());
})) {
return;
}
}
// Retain this script info
toRemoveScriptInfos.delete(info.path);
if (info.sourceMapFilePath) {
let sourceInfos: Map<true> | undefined;
if (isString(info.sourceMapFilePath)) {
// And map file info and source infos
toRemoveScriptInfos.delete(info.sourceMapFilePath);
const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
}
else {
sourceInfos = info.sourceMapFilePath.sourceInfos;
}
if (sourceInfos) {
sourceInfos.forEach((_value, path) => toRemoveScriptInfos.delete(path));
}
}
});
toRemoveScriptInfos.forEach(info => {
// if there are not projects that include this script info - delete it
this.stopWatchingScriptInfo(info);
this.deleteScriptInfo(info);
info.closeSourceMapFileWatcher();
});
}
private telemetryOnOpenFile(scriptInfo: ScriptInfo): void {
if (this.syntaxOnly || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) {
return;

View File

@@ -503,6 +503,16 @@ namespace ts.server {
return this.getLanguageService().getSourceMapper();
}
/*@internal*/
getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
return this.projectService.getDocumentPositionMapper(this, generatedFileName, sourceFileName);
}
/*@internal*/
getSourceFileLike(fileName: string) {
return this.projectService.getSourceFileLike(fileName, this);
}
private shouldEmitFile(scriptInfo: ScriptInfo) {
return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent();
}
@@ -749,7 +759,10 @@ namespace ts.server {
}
containsScriptInfo(info: ScriptInfo): boolean {
return this.isRoot(info) || (!!this.program && this.program.getSourceFileByPath(info.path) !== undefined);
if (this.isRoot(info)) return true;
if (!this.program) return false;
const file = this.program.getSourceFileByPath(info.path);
return !!file && file.resolvedPath === info.path;
}
containsFile(filename: NormalizedPath, requireOpen?: boolean): boolean {

View File

@@ -64,12 +64,22 @@ namespace ts.server {
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?: string) {
this.svc = undefined;
this.text = newText;
this.lineMap = undefined;
this.fileSize = undefined;
this.resetSourceMapInfo();
this.version.text++;
}
@@ -79,6 +89,7 @@ namespace ts.server {
this.text = undefined;
this.lineMap = undefined;
this.fileSize = undefined;
this.resetSourceMapInfo();
}
/**
@@ -156,8 +167,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
@@ -176,9 +187,9 @@ namespace ts.server {
* @param line 1 based index
* @param offset 1 based index
*/
lineOffsetToPosition(line: number, offset: number): number {
lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number {
if (!this.useScriptVersionCacheIfValidOrOpen()) {
return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text);
return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text, allowEdits);
}
// TODO: assert this offset is actually on the line
@@ -246,6 +257,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*/
@@ -259,6 +281,12 @@ namespace ts.server {
sourceFile: SourceFile;
}
/*@internal*/
export interface SourceMapFileWatcher {
watcher: FileWatcher;
sourceInfos?: Map<true>;
}
export class ScriptInfo {
/**
* All projects that include this file
@@ -284,6 +312,20 @@ namespace ts.server {
/*@internal*/
mTime?: number;
/*@internal*/
sourceFileLike?: SourceFileLike;
/*@internal*/
sourceMapFilePath?: Path | SourceMapFileWatcher | false;
// Present on sourceMapFile info
/*@internal*/
declarationInfoPath?: Path;
/*@internal*/
sourceInfos?: Map<true>;
/*@internal*/
documentPositionMapper?: DocumentPositionMapper | false;
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
@@ -521,8 +563,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 {
@@ -551,8 +593,12 @@ namespace ts.server {
* @param line 1 based index
* @param offset 1 based index
*/
lineOffsetToPosition(line: number, offset: number): number {
return this.textStorage.lineOffsetToPosition(line, offset);
lineOffsetToPosition(line: number, offset: number): number;
/*@internal*/
// tslint:disable-next-line:unified-signatures
lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number;
lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number {
return this.textStorage.lineOffsetToPosition(line, offset, allowEdits);
}
positionToLineOffset(position: number): protocol.Location {
@@ -562,5 +608,18 @@ namespace ts.server {
public isJavaScript() {
return this.scriptKind === ScriptKind.JS || this.scriptKind === ScriptKind.JSX;
}
/*@internal*/
getLineInfo(): LineInfo {
return this.textStorage.getLineInfo();
}
/*@internal*/
closeSourceMapFileWatcher() {
if (this.sourceMapFilePath && !isString(this.sourceMapFilePath)) {
closeFileWatcherOf(this.sourceMapFilePath);
this.sourceMapFilePath = undefined;
}
}
}
}

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

@@ -602,8 +602,8 @@ namespace ts {
return getLineStarts(this);
}
public getPositionOfLineAndCharacter(line: number, character: number): number {
return getPositionOfLineAndCharacter(this, line, character);
public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number {
return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits);
}
public getLineEndOfPosition(pos: number): number {
@@ -1139,7 +1139,16 @@ namespace ts {
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const sourceMapper = getSourceMapper(useCaseSensitiveFileNames, currentDirectory, log, host, () => program);
const sourceMapper = getSourceMapper({
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
getCurrentDirectory: () => currentDirectory,
getProgram,
fileExists: host.fileExists && (f => host.fileExists!(f)),
readFile: host.readFile && ((f, encoding) => host.readFile!(f, encoding)),
getDocumentPositionMapper: host.getDocumentPositionMapper && ((generatedFileName, sourceFileName) => host.getDocumentPositionMapper!(generatedFileName, sourceFileName)),
getSourceFileLike: host.getSourceFileLike && (f => host.getSourceFileLike!(f)),
log
});
function getValidSourceFile(fileName: string): SourceFile {
const sourceFile = program.getSourceFile(fileName);
@@ -1203,15 +1212,7 @@ namespace ts {
writeFile: noop,
getCurrentDirectory: () => currentDirectory,
fileExists,
readFile(fileName) {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
if (entry) {
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
}
return host.readFile && host.readFile(fileName);
},
readFile,
realpath: host.realpath && (path => host.realpath!(path)),
directoryExists: directoryName => {
return directoryProbablyExists(directoryName, host);
@@ -1272,6 +1273,16 @@ namespace ts {
(!!host.fileExists && host.fileExists(fileName));
}
function readFile(fileName: string) {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
if (entry) {
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
}
return host.readFile && host.readFile(fileName);
}
// Release any files we have acquired in the old program but are
// not part of the new program.
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {

View File

@@ -9,151 +9,182 @@ namespace ts {
clearCache(): void;
}
export function getSourceMapper(
useCaseSensitiveFileNames: boolean,
currentDirectory: string,
log: (message: string) => void,
host: LanguageServiceHost,
getProgram: () => Program,
): SourceMapper {
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
let sourcemappedFileCache: SourceFileLikeCache;
export interface SourceMapperHost {
useCaseSensitiveFileNames(): boolean;
getCurrentDirectory(): string;
getProgram(): Program | undefined;
fileExists?(path: string): boolean;
readFile?(path: string, encoding?: string): string | undefined;
getSourceFileLike?(fileName: string): SourceFileLike | undefined;
getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined;
log(s: string): void;
}
export function getSourceMapper(host: SourceMapperHost): SourceMapper {
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 = sourcemappedFileCache.get(toPath(fileName));
if (!mappedFile) {
return;
}
function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) {
const path = toPath(generatedFileName);
const value = documentPositionMappers.get(path);
if (value) return value;
return tryGetSourceMappingURL(mappedFile.text, getLineStarts(mappedFile));
}
function convertDocumentToSourceMapper(file: { sourceMapper?: DocumentPositionMapper }, 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(generatedFileName, sourceFileName);
}
const program = getProgram();
return file.sourceMapper = createDocumentPositionMapper({
getSourceFileLike: s => {
// Lookup file in program, if provided
const file = program && program.getSourceFileByPath(s);
// file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file
if (file === undefined || file.resolvedPath !== s) {
// Otherwise check the cache (which may hit disk)
return sourcemappedFileCache.get(s);
}
return file;
},
getCanonicalFileName,
log,
}, map, mapFileName);
}
function getSourceMapper(fileName: string, file: SourceFileLike): DocumentPositionMapper {
if (!host.readFile || !host.fileExists) {
return file.sourceMapper = identitySourceMapConsumer;
else if (host.readFile) {
const file = getSourceFileLike(generatedFileName);
mapper = file && ts.getDocumentPositionMapper(
{ getSourceFileLike, getCanonicalFileName, log: s => host.log(s) },
generatedFileName,
getLineInfo(file.text, getLineStarts(file)),
f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined
);
}
if (file.sourceMapper) {
return file.sourceMapper;
}
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 mapPath = ts.toPath(location, getDirectoryPath(fileName), getCanonicalFileName);
if (host.fileExists(mapPath)) {
return convertDocumentToSourceMapper(file, host.readFile(mapPath)!, mapPath); // TODO: GH#18217
}
}
return file.sourceMapper = identitySourceMapConsumer;
documentPositionMappers.set(path, mapper || identitySourceMapConsumer);
return mapper || identitySourceMapConsumer;
}
function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined {
if (!isDeclarationFileName(info.fileName)) return undefined;
const file = getFile(info.fileName);
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 {
const program = getProgram();
if (isDeclarationFileName(info.fileName)) return undefined;
const sourceFile = getSourceFile(info.fileName);
if (!sourceFile) return undefined;
const program = host.getProgram()!;
const options = program.getCompilerOptions();
const outPath = options.outFile || options.out;
const declarationPath = outPath ?
removeFileExtension(outPath) + Extension.Dts :
getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName);
if (declarationPath === undefined) return undefined;
const declarationFile = getFile(declarationPath);
if (!declarationFile) return undefined;
const newLoc = getSourceMapper(declarationPath, declarationFile).getGeneratedPosition(info);
const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info);
return newLoc === info ? undefined : newLoc;
}
function getFile(fileName: string): SourceFileLike | undefined {
function getSourceFile(fileName: string) {
const program = host.getProgram();
if (!program) return undefined;
const path = toPath(fileName);
const file = getProgram().getSourceFileByPath(path);
if (file && file.resolvedPath === path) {
return file;
// file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file
const file = program.getSourceFileByPath(path);
return file && file.resolvedPath === path ? file : undefined;
}
function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined {
const path = toPath(fileName);
const fileFromCache = sourceFileLike.get(path);
if (fileFromCache !== undefined) return fileFromCache ? fileFromCache : undefined;
if (!host.readFile || host.fileExists && !host.fileExists(path)) {
sourceFileLike.set(path, false);
return undefined;
}
return sourcemappedFileCache.get(path);
// And failing that, check the disk
const text = host.readFile(path);
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 !host.getSourceFileLike ?
getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) :
host.getSourceFileLike(fileName);
}
function toLineColumnOffset(fileName: string, position: number): LineAndCharacter {
const file = getFile(fileName)!; // TODO: GH#18217
const file = getSourceFileLike(fileName)!; // TODO: GH#18217
return file.getLineAndCharacterOfPosition(position);
}
function clearCache(): void {
sourcemappedFileCache = createSourceFileLikeCache(host);
sourceFileLike.clear();
documentPositionMappers.clear();
}
}
interface SourceFileLikeCache {
get(path: Path): SourceFileLike | undefined;
/**
* string | undefined to contents of map file to create DocumentPositionMapper from it
* DocumentPositionMapper | false to give back cached DocumentPositionMapper
*/
export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | DocumentPositionMapper | false;
export function getDocumentPositionMapper(
host: DocumentPositionMapperHost,
generatedFileName: string,
generatedFileLineInfo: LineInfo,
readMapFile: ReadMapFile) {
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");
const originalMapFileName = mapFileName && getNormalizedAbsolutePath(mapFileName, getDirectoryPath(generatedFileName));
for (const location of possibleMapLocations) {
const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName));
const mapFileContents = readMapFile(mapFileName, originalMapFileName);
if (isString(mapFileContents)) {
return convertDocumentToSourceMapper(host, mapFileContents, mapFileName);
}
if (mapFileContents !== undefined) {
return mapFileContents || undefined;
}
}
return undefined;
}
function createSourceFileLikeCache(host: { readFile?: (path: string) => string | undefined, fileExists?: (path: string) => boolean }): SourceFileLikeCache {
const cached = createMap<SourceFileLike>();
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 {
get(path: Path) {
if (cached.has(path)) {
return cached.get(path);
}
if (!host.fileExists || !host.readFile || !host.fileExists(path)) return;
// And failing that, check the disk
const text = host.readFile(path)!; // TODO: GH#18217
const file = {
text,
lineMap: undefined,
getLineAndCharacterOfPosition(pos: number) {
return computeLineAndCharacterOfPosition(getLineStarts(this), pos);
}
} as SourceFileLike;
cached.set(path, file);
return file;
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?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined;
/* @internal */
getSourceFileLike?(fileName: string): SourceFileLike | undefined;
}
/* @internal */

View File

@@ -1282,11 +1282,11 @@ namespace ts {
return !!compilerOptions.module || compilerOptions.target! >= ScriptTarget.ES2015 || !!compilerOptions.noEmit;
}
export function hostUsesCaseSensitiveFileNames(host: LanguageServiceHost): boolean {
export function hostUsesCaseSensitiveFileNames(host: { useCaseSensitiveFileNames?(): boolean; }): boolean {
return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false;
}
export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName {
export function hostGetCanonicalFileName(host: { useCaseSensitiveFileNames?(): boolean; }): GetCanonicalFileName {
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
}

View File

@@ -10,12 +10,16 @@ namespace ts.textStorage {
}`
};
function getDummyScriptInfo() {
return { closeSourceMapFileWatcher: noop } as server.ScriptInfo;
}
it("text based storage should be have exactly the same as script version cache", () => {
const host = projectSystem.createServerHost([f]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
ts1.useScriptVersionCache_TestOnly();
ts2.useText();
@@ -49,7 +53,7 @@ namespace ts.textStorage {
it("should switch to script version cache if necessary", () => {
const host = projectSystem.createServerHost([f]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
ts1.getSnapshot();
assert.isFalse(ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 1");
@@ -60,14 +64,14 @@ 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");
});
it("should be able to return the file size immediately after construction", () => {
const host = projectSystem.createServerHost([f]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
assert.strictEqual(f.content.length, ts1.getTelemetryFileSize());
});
@@ -75,7 +79,7 @@ namespace ts.textStorage {
it("should be able to return the file size when backed by text", () => {
const host = projectSystem.createServerHost([f]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
ts1.useText(f.content);
assert.isFalse(ts1.hasScriptVersionCache_TestOnly());
@@ -86,7 +90,7 @@ namespace ts.textStorage {
it("should be able to return the file size when backed by a script version cache", () => {
const host = projectSystem.createServerHost([f]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, getDummyScriptInfo());
ts1.useScriptVersionCache_TestOnly();
assert.isTrue(ts1.hasScriptVersionCache_TestOnly());
@@ -126,7 +130,7 @@ namespace ts.textStorage {
const host = projectSystem.createServerHost([changingFile]);
// Since script info is not used in these tests, just cheat by passing undefined
const ts1 = new server.TextStorage(host, server.asNormalizedPath(changingFile.path), /*initialVersion*/ undefined, /*info*/undefined!);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(changingFile.path), /*initialVersion*/ undefined, getDummyScriptInfo());
assert.isTrue(ts1.reloadFromDisk());

View File

@@ -475,6 +475,10 @@ namespace ts.projectSystem {
checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path));
}
function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray<string>) {
checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles);
}
function protocolLocationFromSubstring(str: string, substring: string): protocol.Location {
const start = str.indexOf(substring);
Debug.assert(start !== -1);
@@ -10667,7 +10671,7 @@ declare class TestLib {
});
});
it("can go to definition correctly", () => {
describe("with main and depedency project", () => {
const projectLocation = "/user/username/projects/myproject";
const dependecyLocation = `${projectLocation}/dependency`;
const mainLocation = `${projectLocation}/main`;
@@ -10677,7 +10681,8 @@ declare class TestLib {
export function fn2() { }
export function fn3() { }
export function fn4() { }
export function fn5() { }`
export function fn5() { }
`
};
const dependencyConfig: File = {
path: `${dependecyLocation}/tsconfig.json`,
@@ -10687,14 +10692,19 @@ export function fn5() { }`
const mainTs: File = {
path: `${mainLocation}/main.ts`,
content: `import {
fn1, fn2, fn3, fn4, fn5
fn1,
fn2,
fn3,
fn4,
fn5
} from '../dependency/fns'
fn1();
fn2();
fn3();
fn4();
fn5();`
fn5();
`
};
const mainConfig: File = {
path: `${mainLocation}/tsconfig.json`,
@@ -10704,24 +10714,528 @@ fn5();`
})
};
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile];
const host = createHost(files, [mainConfig.path]);
const session = createSession(host);
const service = session.getProjectService();
openFilesForSession([mainTs], session);
checkNumberOfProjects(service, { configuredProjects: 1 });
checkProjectActualFiles(service.configuredProjects.get(mainConfig.path)!, [mainTs.path, libFile.path, mainConfig.path, `${dependecyLocation}/fns.d.ts`]);
for (let i = 0; i < 5; i++) {
const startSpan = { line: i + 5, offset: 1 };
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: { file: mainTs.path, ...startSpan }
}).response as protocol.DefinitionInfoAndBoundSpan;
assert.deepEqual(response, {
definitions: [{ file: dependencyTs.path, start: { line: i + 1, offset: 17 }, end: { line: i + 1, offset: 20 } }],
textSpan: { start: startSpan, end: { line: startSpan.line, offset: startSpan.offset + 3 } }
});
const randomFile: File = {
path: `${projectLocation}/random/random.ts`,
content: "let a = 10;"
};
const randomConfig: File = {
path: `${projectLocation}/random/tsconfig.json`,
content: "{}"
};
const dtsLocation = `${dependecyLocation}/FnS.d.ts`;
const dtsPath = dtsLocation.toLowerCase() as Path;
const dtsMapLocation = `${dtsLocation}.map`;
const dtsMapPath = dtsMapLocation.toLowerCase() as Path;
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos));
checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()));
}
function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path));
}
function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) {
verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]);
}
// Returns request and expected Response, expected response when no map file
interface SessionAction<Req = protocol.Request, Response = {}> {
reqName: string;
request: Partial<Req>;
expectedResponse: Response;
expectedResponseNoMap?: Response;
expectedResponseNoDts?: Response;
}
function gotoDefintinionFromMainTs(fn: number): SessionAction<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionInfoAndBoundSpan> {
const textSpan = usageSpan(fn);
const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) };
const declareSpaceLength = "declare ".length;
return {
reqName: "goToDef",
request: {
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: { file: mainTs.path, ...textSpan.start }
},
expectedResponse: {
// To dependency
definitions: [definition],
textSpan
},
expectedResponseNoMap: {
// To the dts
definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }],
textSpan
},
expectedResponseNoDts: {
// To import declaration
definitions: [{ file: mainTs.path, ...importSpan(fn) }],
textSpan
}
};
}
function definitionSpan(fn: number): protocol.TextSpan {
return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } };
}
function importSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } };
}
function usageSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } };
}
function renameFromDependencyTs(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const triggerSpan = definitionSpan(fn);
return {
reqName: "rename",
request: {
command: protocol.CommandTypes.Rename,
arguments: { file: dependencyTs.path, ...triggerSpan.start }
},
expectedResponse: {
info: {
canRename: true,
fileToRename: undefined,
displayName: `fn${fn}`,
fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
triggerSpan
},
locs: [
{ file: dependencyTs.path, locs: [triggerSpan] }
]
}
};
}
function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const { reqName, request, expectedResponse } = renameFromDependencyTs(fn);
const { info, locs } = expectedResponse;
return {
reqName,
request,
expectedResponse: {
info,
locs: [
locs[0],
{
file: mainTs.path,
locs: [
importSpan(fn),
usageSpan(fn)
]
}
]
},
// Only dependency result
expectedResponseNoMap: expectedResponse,
expectedResponseNoDts: expectedResponse
};
}
// Returns request and expected Response
type SessionActionGetter<Req = protocol.Request, Response = {}> = (fn: number) => SessionAction<Req, Response>;
// Open File, expectedProjectActualFiles, actionGetter, openFileLastLine
interface DocumentPositionMapperVerifier {
openFile: File;
expectedProjectActualFiles: ReadonlyArray<string>;
actionGetter: SessionActionGetter;
openFileLastLine: number;
}
function verifyDocumentPositionMapperUpdates(
mainScenario: string,
verifier: ReadonlyArray<DocumentPositionMapperVerifier>,
closedInfos: ReadonlyArray<string>) {
const openFiles = verifier.map(v => v.openFile);
const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles);
const actionGetters = verifier.map(v => v.actionGetter);
const openFileLastLines = verifier.map(v => v.openFileLastLine);
const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`);
const openInfos = openFiles.map(f => f.path);
// When usage and dependency are used, dependency config is part of closedInfo so ignore
const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles;
function openTsFile(onHostCreate?: (host: TestServerHost) => void) {
const host = createHost(files, [mainConfig.path]);
if (onHostCreate) {
onHostCreate(host);
}
const session = createSession(host);
openFilesForSession([...openFiles, randomFile], session);
return { host, session };
}
function checkProject(session: TestSession, noDts?: true) {
const service = session.getProjectService();
checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length });
configFiles.forEach((configFile, index) => {
checkProjectActualFiles(
service.configuredProjects.get(configFile)!,
noDts ?
expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) :
expectedProjectActualFiles[index]
);
});
}
function verifyInfos(session: TestSession, host: TestServerHost) {
verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles);
}
function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)),
dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles
);
}
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)),
// When project actual file contains dts, it needs to be watched
dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ?
otherWatchedFiles.concat(dtsClosedInfo) :
otherWatchedFiles
);
}
function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) {
assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap);
if (notEqual) {
assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
else {
assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
}
function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) {
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn);
const { response } = session.executeCommandSeq(request);
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts };
}
function firstAction(session: TestSession) {
actionGetters.forEach(actionGetter => action(actionGetter, 1, session));
}
function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType<typeof action>, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) {
// action
let isFirst = true;
for (const actionGetter of actionGetters) {
for (let fn = 1; fn <= 5; fn++) {
const result = action(actionGetter, fn, session);
const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath);
if (dtsAbsent) {
assert.isUndefined(dtsInfo);
}
else {
assert.isDefined(dtsInfo);
}
verifyAction(result, dtsInfo, isFirst);
isFirst = false;
}
}
}
function verifyAllFnAction(
session: TestSession,
host: TestServerHost,
firstDocumentPositionMapperNotEquals?: true,
dependencyMap?: server.ScriptInfo,
documentPositionMapper?: server.ScriptInfo["documentPositionMapper"]
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`);
verifyInfos(session, host);
assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath);
if (isFirst) {
if (dependencyMap) {
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals);
documentPositionMapper = dependencyMap.documentPositionMapper;
}
else {
dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
documentPositionMapper = dependencyMap.documentPositionMapper;
}
}
else {
verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper);
}
});
return { dependencyMap: dependencyMap!, documentPositionMapper };
}
function verifyAllFnActionWithNoMap(
session: TestSession,
host: TestServerHost,
dependencyTsOK?: true
) {
let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"];
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoMapFile(session, host, dependencyTsOK);
assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath));
if (isFirst) {
assert.isNotString(dtsInfo!.sourceMapFilePath);
assert.isNotFalse(dtsInfo!.sourceMapFilePath);
assert.isDefined(dtsInfo!.sourceMapFilePath);
sourceMapFilePath = dtsInfo!.sourceMapFilePath;
}
else {
assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath);
}
});
return sourceMapFilePath;
}
function verifyAllFnActionWithNoDts(
session: TestSession,
host: TestServerHost,
dependencyTsAndMapOk?: true
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => {
assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk);
}, /*dtsAbsent*/ true);
}
function verifyScenarioWithChangesWorker(
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals: true | undefined,
timeoutBeforeAction: boolean
) {
const { host, session } = openTsFile();
// Create DocumentPositionMapper
firstAction(session);
const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
const documentPositionMapper = dependencyMap.documentPositionMapper;
// change
change(host, session);
if (timeoutBeforeAction) {
host.runQueuedTimeoutCallbacks();
checkProject(session);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
}
// action
verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper);
}
function verifyScenarioWithChanges(
scenarioName: string,
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals?: true
) {
describe(scenarioName, () => {
it("when timeout occurs before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true);
});
it("when timeout does not occur before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false);
});
});
}
function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) {
// Main scenario action
const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host);
checkProject(session);
verifyInfos(session, host);
// Collecting at this point retains dependency.d.ts and map
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfos(session, host);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) {
// Main scenario action
verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoMapFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
// Main scenario action
verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoDtsFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyScenarioWhenFileNotPresent(
scenarioName: string,
fileLocation: string,
verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void,
noDts?: true
) {
describe(scenarioName, () => {
it(mainScenario, () => {
const { host, session } = openTsFile(host => host.deleteFile(fileLocation));
checkProject(session, noDts);
verifyScenarioAndScriptInfoCollection(session, host);
});
it("when file is created", () => {
let fileContents: string | undefined;
const { host, session } = openTsFile(host => {
fileContents = host.readFile(fileLocation);
host.deleteFile(fileLocation);
});
firstAction(session);
host.writeFile(fileLocation, fileContents!);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
it("when file is deleted", () => {
const { host, session } = openTsFile();
firstAction(session);
// The dependency file is deleted when orphan files are collected
host.deleteFile(fileLocation);
verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true);
});
});
}
it(mainScenario, () => {
const { host, session } = openTsFile();
checkProject(session);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
// Edit
verifyScenarioWithChanges(
"when usage file changes, document position mapper doesnt change",
(_host, session) => openFiles.forEach(
(openFile, index) => session.executeCommandSeq<protocol.ChangeRequest>({
command: protocol.CommandTypes.Change,
arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" }
})
)
);
// Edit dts to add new fn
verifyScenarioWithChanges(
"when dependency .d.ts changes, document position mapper doesnt change",
host => host.writeFile(
dtsLocation,
host.readFile(dtsLocation)!.replace(
"//# sourceMappingURL=FnS.d.ts.map",
`export declare function fn6(): void;
//# sourceMappingURL=FnS.d.ts.map`
)
)
);
// Edit map file to represent added new line
verifyScenarioWithChanges(
"when dependency file's map changes",
host => host.writeFile(
dtsMapLocation,
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
),
/*afterActionDocumentPositionMapperNotEquals*/ true
);
verifyScenarioWhenFileNotPresent(
"when map file is not present",
dtsMapLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoMap
);
verifyScenarioWhenFileNotPresent(
"when .d.ts file is not present",
dtsLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoDts,
/*noDts*/ true
);
}
const usageVerifier: DocumentPositionMapperVerifier = {
openFile: mainTs,
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
actionGetter: gotoDefintinionFromMainTs,
openFileLastLine: 14
};
describe("from project that uses dependency", () => {
const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"can go to definition correctly",
[usageVerifier],
closedInfos
);
});
const definingVerifier: DocumentPositionMapperVerifier = {
openFile: dependencyTs,
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
actionGetter: renameFromDependencyTs,
openFileLastLine: 6
};
describe("from defining project", () => {
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"rename locations from dependency",
[definingVerifier],
closedInfos
);
});
describe("when opening depedency and usage project", () => {
const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path];
verifyDocumentPositionMapperUpdates(
"goto Definition in usage and rename locations from defining project",
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
closedInfos
);
});
});
});

View File

@@ -8497,10 +8497,6 @@ declare namespace ts.server {
syntaxOnly?: boolean;
}
class ProjectService {
/**
* Container of all known scripts
*/
private readonly filenameToScriptInfo;
private readonly scriptInfoInNodeModulesWatchers;
/**
* Contains all the deleted script info's version information so that
@@ -8599,6 +8595,9 @@ declare namespace ts.server {
getHostFormatCodeOptions(): FormatCodeSettings;
getHostPreferences(): protocol.UserPreferences;
private onSourceFileChanged;
private handleSourceMapProjects;
private delayUpdateSourceInfoProjects;
private delayUpdateProjectsOfScriptInfoPath;
private handleDeletedFile;
private onConfigChangedForConfiguredProject;
/**
@@ -8689,6 +8688,8 @@ declare namespace ts.server {
*/
getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined;
getScriptInfoForPath(fileName: Path): ScriptInfo | undefined;
private addSourceInfoToSourceMap;
private addMissingSourceMapFile;
setHostConfiguration(args: protocol.ConfigureRequestArguments): void;
closeLog(): void;
/**
@@ -8726,6 +8727,7 @@ declare namespace ts.server {
private findExternalProjectContainingOpenScriptInfo;
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult;
private removeOrphanConfiguredProjects;
private removeOrphanScriptInfos;
private telemetryOnOpenFile;
/**
* Close file whose contents is managed by the client