Rename through all projects with the same file symLink

This commit is contained in:
Sheetal Nandi 2018-01-12 13:54:49 -08:00
parent ef7f131398
commit 428e0529fd
7 changed files with 226 additions and 101 deletions

View File

@ -524,7 +524,12 @@ namespace ts {
process.exit(exitCode);
},
realpath(path: string): string {
return _fs.realpathSync(path);
try {
return _fs.realpathSync(path);
}
catch {
return path;
}
},
debugMode: some(<string[]>process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)),
tryEnableSourceMapsForHost() {

View File

@ -6604,6 +6604,7 @@ namespace ts.projectSystem {
const host = createServerHost(files);
const session = createSession(host);
const projectService = session.getProjectService();
debugger;
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: {
@ -6637,6 +6638,7 @@ namespace ts.projectSystem {
assert.isDefined(projectService.configuredProjects.get(aTsconfig.path));
assert.isDefined(projectService.configuredProjects.get(bTsconfig.path));
debugger;
verifyRenameResponse(session.executeCommandSeq<protocol.RenameRequest>({
command: protocol.CommandTypes.Rename,
arguments: {
@ -6650,20 +6652,25 @@ namespace ts.projectSystem {
function verifyRenameResponse({ info, locs }: protocol.RenameResponseBody) {
assert.isTrue(info.canRename);
assert.equal(locs.length, 2); // Currently 2 but needs to be 4
assert.deepEqual(locs[0], {
file: aFile.path,
locs: [
{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 40 } },
{ start: { line: 1, offset: 9 }, end: { line: 1, offset: 10 } }
]
});
assert.deepEqual(locs[1], {
file: aFc,
locs: [
{ start: { line: 1, offset: 14 }, end: { line: 1, offset: 15 } }
]
});
assert.equal(locs.length, 4);
verifyLocations(0, aFile.path, aFc);
verifyLocations(2, bFile.path, bFc);
function verifyLocations(locStartIndex: number, firstFile: string, secondFile: string) {
assert.deepEqual(locs[locStartIndex], {
file: firstFile,
locs: [
{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 40 } },
{ start: { line: 1, offset: 9 }, end: { line: 1, offset: 10 } }
]
});
assert.deepEqual(locs[locStartIndex + 1], {
file: secondFile,
locs: [
{ start: { line: 1, offset: 14 }, end: { line: 1, offset: 15 } }
]
});
}
}
});
});

View File

@ -88,7 +88,7 @@ interface Array<T> {}`
}
interface SymLink extends FSEntry {
symLink: Path;
symLink: string;
}
function isFolder(s: FSEntry): s is Folder {
@ -526,7 +526,7 @@ interface Array<T> {}`
return {
path: this.toPath(fullPath),
fullPath,
symLink: this.toPath(getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath)))
symLink: getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath))
};
}
@ -539,17 +539,13 @@ interface Array<T> {}`
};
}
private isFile(fsEntry: FSEntry) {
return !!this.getRealFile(fsEntry.path, fsEntry);
}
private getRealFile(path: Path, fsEntry = this.fs.get(path)): File | undefined {
if (isFile(fsEntry)) {
private getRealFsEntry<T extends FSEntry>(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)): T | undefined {
if (isFsEntry(fsEntry)) {
return fsEntry;
}
if (isSymLink(fsEntry)) {
return this.getRealFile(fsEntry.symLink);
return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink));
}
if (fsEntry) {
@ -557,42 +553,28 @@ interface Array<T> {}`
return undefined;
}
const dir = getDirectoryPath(path);
const dirEntry = this.getRealFolder(dir);
if (dirEntry && dirEntry.path !== dir) {
return this.getRealFile(combinePaths(dirEntry.path, getBaseFileName(path)) as Path);
const realpath = this.realpath(path);
if (path !== realpath) {
return this.getRealFsEntry(isFsEntry, realpath as Path);
}
return undefined;
}
private isFile(fsEntry: FSEntry) {
return !!this.getRealFile(fsEntry.path, fsEntry);
}
private getRealFile(path: Path, fsEntry?: FSEntry): File | undefined {
return this.getRealFsEntry(isFile, path, fsEntry);
}
private isFolder(fsEntry: FSEntry) {
return !!this.getRealFolder(fsEntry.path, fsEntry);
}
private getRealFolder(path: Path, fsEntry = this.fs.get(path)): Folder | undefined {
if (isFolder(fsEntry)) {
return fsEntry;
}
if (isSymLink(fsEntry)) {
return this.getRealFolder(fsEntry.symLink);
}
if (fsEntry) {
// This fs entry is something else
return undefined;
}
const baseName = getBaseFileName(path);
const dir = getDirectoryPath(path);
if (dir !== baseName) {
const dirEntry = this.getRealFolder(dir);
if (dirEntry && dirEntry.path !== dir) {
return this.getRealFolder(combinePaths(dirEntry.path, baseName) as Path);
}
}
return undefined;
return this.getRealFsEntry(isFolder, path, fsEntry);
}
fileExists(s: string) {
@ -765,16 +747,21 @@ interface Array<T> {}`
clear(this.output);
}
realpath(s: string) {
while (true) {
const fsEntry = this.fs.get(this.toFullPath(s));
if (isSymLink(fsEntry)) {
s = fsEntry.symLink;
}
else {
return s;
}
realpath(s: string): string {
const fullPath = this.toNormalizedAbsolutePath(s);
const path = this.toPath(fullPath);
if (getDirectoryPath(path) === path) {
// Root
return s;
}
const dirFullPath = this.realpath(getDirectoryPath(fullPath));
const realFullPath = combinePaths(dirFullPath, getBaseFileName(fullPath));
const fsEntry = this.fs.get(this.toPath(realFullPath));
if (isSymLink(fsEntry)) {
return this.realpath(fsEntry.symLink);
}
return realFullPath;
}
readonly existMessage = "System Exit";

View File

@ -198,16 +198,6 @@ namespace ts.server {
}
}
/**
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
export function combineProjectOutput<T>(projects: ReadonlyArray<Project>, action: (project: Project) => ReadonlyArray<T>, comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) {
const outputs = flatMap(projects, action);
return comparer
? sortAndDeduplicate(outputs, comparer, areEqual)
: deduplicate(outputs, areEqual);
}
export interface HostConfiguration {
formatCodeOptions: FormatCodeSettings;
hostInfo: string;
@ -335,6 +325,11 @@ namespace ts.server {
* Container of all known scripts
*/
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
/**
* Map to the real path of the infos
*/
/* @internal */
readonly realpathToScriptInfos: MultiMap<ScriptInfo> | undefined;
/**
* maps external project file name to list of config files that were the part of this project
*/
@ -427,7 +422,9 @@ namespace ts.server {
this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(this.getExecutingFilePath(), "../typesMap.json") : opts.typesMapLocation;
Debug.assert(!!this.host.createHash, "'ServerHost.createHash' is required for ProjectService");
if (this.host.realpath) {
this.realpathToScriptInfos = createMultiMap();
}
this.currentDirectory = this.host.getCurrentDirectory();
this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames);
this.throttledOperations = new ThrottledOperations(this.host, this.logger);
@ -768,7 +765,7 @@ namespace ts.server {
if (info.containingProjects.length === 0) {
// Orphan script info, remove it as we can always reload it on next open file request
this.stopWatchingScriptInfo(info);
this.filenameToScriptInfo.delete(info.path);
this.deleteScriptInfo(info);
}
else {
// file has been changed which might affect the set of referenced files in projects that include
@ -785,7 +782,7 @@ namespace ts.server {
// TODO: handle isOpen = true case
if (!info.isScriptOpen()) {
this.filenameToScriptInfo.delete(info.path);
this.deleteScriptInfo(info);
// capture list of projects since detachAllProjects will wipe out original list
const containingProjects = info.containingProjects.slice();
@ -1019,11 +1016,19 @@ namespace ts.server {
if (!info.isScriptOpen() && info.isOrphan()) {
// if there are not projects that include this script info - delete it
this.stopWatchingScriptInfo(info);
this.filenameToScriptInfo.delete(info.path);
this.deleteScriptInfo(info);
}
});
}
private deleteScriptInfo(info: ScriptInfo) {
this.filenameToScriptInfo.delete(info.path);
const realpath = info.getRealpathIfDifferent();
if (realpath) {
this.realpathToScriptInfos.remove(realpath, info);
}
}
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) {
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
if (configFileExistenceInfo) {
@ -1736,6 +1741,43 @@ namespace ts.server {
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
}
/**
* Returns the projects that contain script info through SymLink
* Note that this does not return projects in info.containingProjects
*/
/*@internal*/
getSymlinkedProjects(info: ScriptInfo): MultiMap<Project> | undefined {
let projects: MultiMap<Project> | undefined;
if (this.realpathToScriptInfos) {
const realpath = info.getRealpathIfDifferent();
if (realpath) {
forEach(this.realpathToScriptInfos.get(realpath), combineProjects);
}
forEach(this.realpathToScriptInfos.get(info.path), combineProjects);
}
return projects;
function combineProjects(toAddInfo: ScriptInfo) {
if (toAddInfo !== info) {
for (const project of toAddInfo.containingProjects) {
// Add the projects only if they can use symLink targets and not already in the list
if (project.languageServiceEnabled &&
!project.getCompilerOptions().preserveSymlinks &&
!contains(info.containingProjects, project)) {
if (!projects) {
projects = createMultiMap();
projects.add(toAddInfo.path, project);
}
else if (!forEachEntry(projects, (projs, path) => path === toAddInfo.path ? false : contains(projs, project))) {
projects.add(toAddInfo.path, project);
}
}
}
}
}
}
private watchClosedScriptInfo(info: ScriptInfo) {
Debug.assert(!info.fileWatcher);
// do not watch files with mixed content - server doesn't know how to interpret it

View File

@ -213,6 +213,10 @@ namespace ts.server {
/*@internal*/
readonly isDynamic: boolean;
/*@internal*/
/** Set to real path if path is different from info.path */
private realpath: Path | undefined;
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
@ -224,6 +228,7 @@ namespace ts.server {
this.textStorage = new TextStorage(host, fileName);
if (hasMixedContent || this.isDynamic) {
this.textStorage.reload("");
this.realpath = this.path;
}
this.scriptKind = scriptKind
? scriptKind
@ -264,6 +269,30 @@ namespace ts.server {
return this.textStorage.getSnapshot();
}
private ensureRealPath() {
if (this.realpath === undefined) {
// Default is just the path
this.realpath = this.path;
if (this.host.realpath) {
Debug.assert(!!this.containingProjects.length);
const project = this.containingProjects[0];
const realpath = this.host.realpath(this.path);
if (realpath) {
this.realpath = project.toPath(realpath);
// If it is different from this.path, add to the map
if (this.realpath !== this.path) {
project.projectService.realpathToScriptInfos.add(this.realpath, this);
}
}
}
}
}
/*@internal*/
getRealpathIfDifferent(): Path | undefined {
return this.realpath && this.realpath !== this.path ? this.realpath : undefined;
}
getFormatCodeSettings() {
return this.formatCodeSettings;
}
@ -272,6 +301,9 @@ namespace ts.server {
const isNew = !this.isAttached(project);
if (isNew) {
this.containingProjects.push(project);
if (!project.getCompilerOptions().preserveSymlinks) {
this.ensureRealPath();
}
}
return isNew;
}

View File

@ -255,6 +255,32 @@ namespace ts.server {
};
}
type Projects = ReadonlyArray<Project> | {
projects: ReadonlyArray<Project>;
symLinkedProjects: MultiMap<Project>;
};
function isProjectsArray(projects: Projects): projects is ReadonlyArray<Project> {
return !!(<ReadonlyArray<Project>>projects).length;
}
/**
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
function combineProjectOutput<T, U>(defaultValue: T, getValue: (path: Path) => T, projects: Projects, action: (project: Project, value: T) => ReadonlyArray<U> | U | undefined, comparer?: (a: U, b: U) => number, areEqual?: (a: U, b: U) => boolean) {
const outputs = flatMap(isProjectsArray(projects) ? projects : projects.projects, project => action(project, defaultValue));
if (!isProjectsArray(projects) && projects.symLinkedProjects) {
projects.symLinkedProjects.forEach((projects, path) => {
const value = getValue(path as Path);
outputs.push(...flatMap(projects, project => action(project, value)));
});
}
return comparer
? sortAndDeduplicate(outputs, comparer, areEqual)
: deduplicate(outputs, areEqual);
}
export interface SessionOptions {
host: ServerHost;
cancellationToken: ServerCancellationToken;
@ -789,8 +815,9 @@ namespace ts.server {
return project.getLanguageService().getRenameInfo(file, position);
}
private getProjects(args: protocol.FileRequestArgs) {
let projects: Project[];
private getProjects(args: protocol.FileRequestArgs): Projects {
let projects: ReadonlyArray<Project>;
let symLinkedProjects: MultiMap<Project> | undefined;
if (args.projectFileName) {
const project = this.getProject(args.projectFileName);
if (project) {
@ -800,13 +827,14 @@ namespace ts.server {
else {
const scriptInfo = this.projectService.getScriptInfo(args.file);
projects = scriptInfo.containingProjects;
symLinkedProjects = this.projectService.getSymlinkedProjects(scriptInfo);
}
// filter handles case when 'projects' is undefined
projects = filter(projects, p => p.languageServiceEnabled);
if (!projects || !projects.length) {
if ((!projects || !projects.length) && !symLinkedProjects) {
return Errors.ThrowNoProject();
}
return projects;
return symLinkedProjects ? { projects, symLinkedProjects } : projects;
}
private getDefaultProject(args: protocol.FileRequestArgs) {
@ -841,8 +869,10 @@ namespace ts.server {
}
const fileSpans = combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path).fileName,
projects,
(project: Project) => {
(project, file) => {
const renameLocations = project.getLanguageService().findRenameLocations(file, position, args.findInStrings, args.findInComments);
if (!renameLocations) {
return emptyArray;
@ -881,8 +911,10 @@ namespace ts.server {
}
else {
return combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path).fileName,
projects,
p => p.getLanguageService().findRenameLocations(file, position, args.findInStrings, args.findInComments),
(p, file) => p.getLanguageService().findRenameLocations(file, position, args.findInStrings, args.findInComments),
/*comparer*/ undefined,
renameLocationIsEqualTo
);
@ -938,9 +970,11 @@ namespace ts.server {
const nameSpan = nameInfo.textSpan;
const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset;
const nameText = scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan));
const refs = combineProjectOutput<protocol.ReferencesResponseItem>(
const refs = combineProjectOutput<NormalizedPath, protocol.ReferencesResponseItem>(
file,
path => this.projectService.getScriptInfoForPath(path).fileName,
projects,
(project: Project) => {
(project, file) => {
const references = project.getLanguageService().getReferencesAtPosition(file, position);
if (!references) {
return emptyArray;
@ -974,8 +1008,10 @@ namespace ts.server {
}
else {
return combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path).fileName,
projects,
project => project.getLanguageService().findReferences(file, position),
(project, file) => project.getLanguageService().findReferences(file, position),
/*comparer*/ undefined,
equateValues
);
@ -1240,20 +1276,25 @@ namespace ts.server {
return emptyArray;
}
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
// if specified a project, we only return affected file list in this project
const projectsToSearch = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
for (const project of projectsToSearch) {
if (project.compileOnSaveEnabled && project.languageServiceEnabled && !project.getCompilationSettings().noEmit) {
result.push({
projectFileName: project.getProjectName(),
fileNames: project.getCompileOnSaveAffectedFileList(info),
projectUsesOutFile: !!project.getCompilationSettings().outFile || !!project.getCompilationSettings().out
});
const projects = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
const symLinkedProjects = !args.projectFileName && this.projectService.getSymlinkedProjects(info);
return combineProjectOutput(
info,
path => this.projectService.getScriptInfoForPath(path),
symLinkedProjects ? { projects, symLinkedProjects } : projects,
(project, info) => {
let result: protocol.CompileOnSaveAffectedFileListSingleProject;
if (project.compileOnSaveEnabled && project.languageServiceEnabled && !project.getCompilationSettings().noEmit) {
result = {
projectFileName: project.getProjectName(),
fileNames: project.getCompileOnSaveAffectedFileList(info),
projectUsesOutFile: !!project.getCompilationSettings().outFile || !!project.getCompilationSettings().out
};
}
return result;
}
}
return result;
);
}
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs) {
@ -1406,8 +1447,14 @@ namespace ts.server {
const fileName = args.currentFileOnly ? args.file && normalizeSlashes(args.file) : undefined;
if (simplifiedResult) {
return combineProjectOutput(
fileName,
() => undefined,
projects,
project => {
(project, file) => {
if (fileName && !file) {
return undefined;
}
const navItems = project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject());
if (!navItems) {
return emptyArray;
@ -1443,8 +1490,15 @@ namespace ts.server {
}
else {
return combineProjectOutput(
fileName,
() => undefined,
projects,
project => project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject()),
(project, file) => {
if (fileName && !file) {
return undefined;
}
return project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject());
},
/*comparer*/ undefined,
navigateToItemIsEqualTo);
}

View File

@ -7151,6 +7151,7 @@ declare namespace ts.server {
open(newText: string): void;
close(fileExists?: boolean): void;
getSnapshot(): IScriptSnapshot;
private ensureRealPath();
getFormatCodeSettings(): FormatCodeSettings;
attachToProject(project: Project): boolean;
isAttached(project: Project): boolean;
@ -7523,10 +7524,6 @@ declare namespace ts.server {
function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin;
function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind;
function convertScriptKindName(scriptKindName: protocol.ScriptKindName): ScriptKind.Unknown | ScriptKind.JS | ScriptKind.JSX | ScriptKind.TS | ScriptKind.TSX;
/**
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
function combineProjectOutput<T>(projects: ReadonlyArray<Project>, action: (project: Project) => ReadonlyArray<T>, comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean): T[];
interface HostConfiguration {
formatCodeOptions: FormatCodeSettings;
hostInfo: string;
@ -7662,6 +7659,7 @@ declare namespace ts.server {
*/
private closeOpenFile(info);
private deleteOrphanScriptInfoNotInAnyProject();
private deleteScriptInfo(info);
private configFileExists(configFileName, canonicalConfigFilePath, info);
private setConfigFileExistenceByNewConfiguredProject(project);
/**