mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-11 06:02:53 -05:00
Merge pull request #28886 from Microsoft/sourceMapDecoder
Enhancements to SourceMap decoder from tsserver
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user