Skip to content

Commit 428e052

Browse files
committed
Rename through all projects with the same file symLink
1 parent ef7f131 commit 428e052

File tree

7 files changed

+226
-101
lines changed

7 files changed

+226
-101
lines changed

src/compiler/sys.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,12 @@ namespace ts {
524524
process.exit(exitCode);
525525
},
526526
realpath(path: string): string {
527-
return _fs.realpathSync(path);
527+
try {
528+
return _fs.realpathSync(path);
529+
}
530+
catch {
531+
return path;
532+
}
528533
},
529534
debugMode: some(<string[]>process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)),
530535
tryEnableSourceMapsForHost() {

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6604,6 +6604,7 @@ namespace ts.projectSystem {
66046604
const host = createServerHost(files);
66056605
const session = createSession(host);
66066606
const projectService = session.getProjectService();
6607+
debugger;
66076608
session.executeCommandSeq<protocol.OpenRequest>({
66086609
command: protocol.CommandTypes.Open,
66096610
arguments: {
@@ -6637,6 +6638,7 @@ namespace ts.projectSystem {
66376638
assert.isDefined(projectService.configuredProjects.get(aTsconfig.path));
66386639
assert.isDefined(projectService.configuredProjects.get(bTsconfig.path));
66396640

6641+
debugger;
66406642
verifyRenameResponse(session.executeCommandSeq<protocol.RenameRequest>({
66416643
command: protocol.CommandTypes.Rename,
66426644
arguments: {
@@ -6650,20 +6652,25 @@ namespace ts.projectSystem {
66506652

66516653
function verifyRenameResponse({ info, locs }: protocol.RenameResponseBody) {
66526654
assert.isTrue(info.canRename);
6653-
assert.equal(locs.length, 2); // Currently 2 but needs to be 4
6654-
assert.deepEqual(locs[0], {
6655-
file: aFile.path,
6656-
locs: [
6657-
{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 40 } },
6658-
{ start: { line: 1, offset: 9 }, end: { line: 1, offset: 10 } }
6659-
]
6660-
});
6661-
assert.deepEqual(locs[1], {
6662-
file: aFc,
6663-
locs: [
6664-
{ start: { line: 1, offset: 14 }, end: { line: 1, offset: 15 } }
6665-
]
6666-
});
6655+
assert.equal(locs.length, 4);
6656+
verifyLocations(0, aFile.path, aFc);
6657+
verifyLocations(2, bFile.path, bFc);
6658+
6659+
function verifyLocations(locStartIndex: number, firstFile: string, secondFile: string) {
6660+
assert.deepEqual(locs[locStartIndex], {
6661+
file: firstFile,
6662+
locs: [
6663+
{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 40 } },
6664+
{ start: { line: 1, offset: 9 }, end: { line: 1, offset: 10 } }
6665+
]
6666+
});
6667+
assert.deepEqual(locs[locStartIndex + 1], {
6668+
file: secondFile,
6669+
locs: [
6670+
{ start: { line: 1, offset: 14 }, end: { line: 1, offset: 15 } }
6671+
]
6672+
});
6673+
}
66676674
}
66686675
});
66696676
});

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 32 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ interface Array<T> {}`
8888
}
8989

9090
interface SymLink extends FSEntry {
91-
symLink: Path;
91+
symLink: string;
9292
}
9393

9494
function isFolder(s: FSEntry): s is Folder {
@@ -526,7 +526,7 @@ interface Array<T> {}`
526526
return {
527527
path: this.toPath(fullPath),
528528
fullPath,
529-
symLink: this.toPath(getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath)))
529+
symLink: getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath))
530530
};
531531
}
532532

@@ -539,60 +539,42 @@ interface Array<T> {}`
539539
};
540540
}
541541

542-
private isFile(fsEntry: FSEntry) {
543-
return !!this.getRealFile(fsEntry.path, fsEntry);
544-
}
545-
546-
private getRealFile(path: Path, fsEntry = this.fs.get(path)): File | undefined {
547-
if (isFile(fsEntry)) {
542+
private getRealFsEntry<T extends FSEntry>(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)): T | undefined {
543+
if (isFsEntry(fsEntry)) {
548544
return fsEntry;
549545
}
550546

551547
if (isSymLink(fsEntry)) {
552-
return this.getRealFile(fsEntry.symLink);
548+
return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink));
553549
}
554550

555551
if (fsEntry) {
556552
// This fs entry is something else
557553
return undefined;
558554
}
559555

560-
const dir = getDirectoryPath(path);
561-
const dirEntry = this.getRealFolder(dir);
562-
if (dirEntry && dirEntry.path !== dir) {
563-
return this.getRealFile(combinePaths(dirEntry.path, getBaseFileName(path)) as Path);
556+
const realpath = this.realpath(path);
557+
if (path !== realpath) {
558+
return this.getRealFsEntry(isFsEntry, realpath as Path);
564559
}
560+
565561
return undefined;
566562
}
567563

564+
private isFile(fsEntry: FSEntry) {
565+
return !!this.getRealFile(fsEntry.path, fsEntry);
566+
}
567+
568+
private getRealFile(path: Path, fsEntry?: FSEntry): File | undefined {
569+
return this.getRealFsEntry(isFile, path, fsEntry);
570+
}
571+
568572
private isFolder(fsEntry: FSEntry) {
569573
return !!this.getRealFolder(fsEntry.path, fsEntry);
570574
}
571575

572576
private getRealFolder(path: Path, fsEntry = this.fs.get(path)): Folder | undefined {
573-
if (isFolder(fsEntry)) {
574-
return fsEntry;
575-
}
576-
577-
if (isSymLink(fsEntry)) {
578-
return this.getRealFolder(fsEntry.symLink);
579-
}
580-
581-
if (fsEntry) {
582-
// This fs entry is something else
583-
return undefined;
584-
}
585-
586-
const baseName = getBaseFileName(path);
587-
const dir = getDirectoryPath(path);
588-
if (dir !== baseName) {
589-
const dirEntry = this.getRealFolder(dir);
590-
if (dirEntry && dirEntry.path !== dir) {
591-
return this.getRealFolder(combinePaths(dirEntry.path, baseName) as Path);
592-
}
593-
}
594-
595-
return undefined;
577+
return this.getRealFsEntry(isFolder, path, fsEntry);
596578
}
597579

598580
fileExists(s: string) {
@@ -765,16 +747,21 @@ interface Array<T> {}`
765747
clear(this.output);
766748
}
767749

768-
realpath(s: string) {
769-
while (true) {
770-
const fsEntry = this.fs.get(this.toFullPath(s));
771-
if (isSymLink(fsEntry)) {
772-
s = fsEntry.symLink;
773-
}
774-
else {
775-
return s;
776-
}
750+
realpath(s: string): string {
751+
const fullPath = this.toNormalizedAbsolutePath(s);
752+
const path = this.toPath(fullPath);
753+
if (getDirectoryPath(path) === path) {
754+
// Root
755+
return s;
756+
}
757+
const dirFullPath = this.realpath(getDirectoryPath(fullPath));
758+
const realFullPath = combinePaths(dirFullPath, getBaseFileName(fullPath));
759+
const fsEntry = this.fs.get(this.toPath(realFullPath));
760+
if (isSymLink(fsEntry)) {
761+
return this.realpath(fsEntry.symLink);
777762
}
763+
764+
return realFullPath;
778765
}
779766

780767
readonly existMessage = "System Exit";

src/server/editorServices.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,6 @@ namespace ts.server {
198198
}
199199
}
200200

201-
/**
202-
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
203-
*/
204-
export function combineProjectOutput<T>(projects: ReadonlyArray<Project>, action: (project: Project) => ReadonlyArray<T>, comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) {
205-
const outputs = flatMap(projects, action);
206-
return comparer
207-
? sortAndDeduplicate(outputs, comparer, areEqual)
208-
: deduplicate(outputs, areEqual);
209-
}
210-
211201
export interface HostConfiguration {
212202
formatCodeOptions: FormatCodeSettings;
213203
hostInfo: string;
@@ -335,6 +325,11 @@ namespace ts.server {
335325
* Container of all known scripts
336326
*/
337327
private readonly filenameToScriptInfo = createMap<ScriptInfo>();
328+
/**
329+
* Map to the real path of the infos
330+
*/
331+
/* @internal */
332+
readonly realpathToScriptInfos: MultiMap<ScriptInfo> | undefined;
338333
/**
339334
* maps external project file name to list of config files that were the part of this project
340335
*/
@@ -427,7 +422,9 @@ namespace ts.server {
427422
this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(this.getExecutingFilePath(), "../typesMap.json") : opts.typesMapLocation;
428423

429424
Debug.assert(!!this.host.createHash, "'ServerHost.createHash' is required for ProjectService");
430-
425+
if (this.host.realpath) {
426+
this.realpathToScriptInfos = createMultiMap();
427+
}
431428
this.currentDirectory = this.host.getCurrentDirectory();
432429
this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames);
433430
this.throttledOperations = new ThrottledOperations(this.host, this.logger);
@@ -768,7 +765,7 @@ namespace ts.server {
768765
if (info.containingProjects.length === 0) {
769766
// Orphan script info, remove it as we can always reload it on next open file request
770767
this.stopWatchingScriptInfo(info);
771-
this.filenameToScriptInfo.delete(info.path);
768+
this.deleteScriptInfo(info);
772769
}
773770
else {
774771
// file has been changed which might affect the set of referenced files in projects that include
@@ -785,7 +782,7 @@ namespace ts.server {
785782
// TODO: handle isOpen = true case
786783

787784
if (!info.isScriptOpen()) {
788-
this.filenameToScriptInfo.delete(info.path);
785+
this.deleteScriptInfo(info);
789786

790787
// capture list of projects since detachAllProjects will wipe out original list
791788
const containingProjects = info.containingProjects.slice();
@@ -1019,11 +1016,19 @@ namespace ts.server {
10191016
if (!info.isScriptOpen() && info.isOrphan()) {
10201017
// if there are not projects that include this script info - delete it
10211018
this.stopWatchingScriptInfo(info);
1022-
this.filenameToScriptInfo.delete(info.path);
1019+
this.deleteScriptInfo(info);
10231020
}
10241021
});
10251022
}
10261023

1024+
private deleteScriptInfo(info: ScriptInfo) {
1025+
this.filenameToScriptInfo.delete(info.path);
1026+
const realpath = info.getRealpathIfDifferent();
1027+
if (realpath) {
1028+
this.realpathToScriptInfos.remove(realpath, info);
1029+
}
1030+
}
1031+
10271032
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) {
10281033
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
10291034
if (configFileExistenceInfo) {
@@ -1736,6 +1741,43 @@ namespace ts.server {
17361741
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
17371742
}
17381743

1744+
/**
1745+
* Returns the projects that contain script info through SymLink
1746+
* Note that this does not return projects in info.containingProjects
1747+
*/
1748+
/*@internal*/
1749+
getSymlinkedProjects(info: ScriptInfo): MultiMap<Project> | undefined {
1750+
let projects: MultiMap<Project> | undefined;
1751+
if (this.realpathToScriptInfos) {
1752+
const realpath = info.getRealpathIfDifferent();
1753+
if (realpath) {
1754+
forEach(this.realpathToScriptInfos.get(realpath), combineProjects);
1755+
}
1756+
forEach(this.realpathToScriptInfos.get(info.path), combineProjects);
1757+
}
1758+
1759+
return projects;
1760+
1761+
function combineProjects(toAddInfo: ScriptInfo) {
1762+
if (toAddInfo !== info) {
1763+
for (const project of toAddInfo.containingProjects) {
1764+
// Add the projects only if they can use symLink targets and not already in the list
1765+
if (project.languageServiceEnabled &&
1766+
!project.getCompilerOptions().preserveSymlinks &&
1767+
!contains(info.containingProjects, project)) {
1768+
if (!projects) {
1769+
projects = createMultiMap();
1770+
projects.add(toAddInfo.path, project);
1771+
}
1772+
else if (!forEachEntry(projects, (projs, path) => path === toAddInfo.path ? false : contains(projs, project))) {
1773+
projects.add(toAddInfo.path, project);
1774+
}
1775+
}
1776+
}
1777+
}
1778+
}
1779+
}
1780+
17391781
private watchClosedScriptInfo(info: ScriptInfo) {
17401782
Debug.assert(!info.fileWatcher);
17411783
// do not watch files with mixed content - server doesn't know how to interpret it

src/server/scriptInfo.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ namespace ts.server {
213213
/*@internal*/
214214
readonly isDynamic: boolean;
215215

216+
/*@internal*/
217+
/** Set to real path if path is different from info.path */
218+
private realpath: Path | undefined;
219+
216220
constructor(
217221
private readonly host: ServerHost,
218222
readonly fileName: NormalizedPath,
@@ -224,6 +228,7 @@ namespace ts.server {
224228
this.textStorage = new TextStorage(host, fileName);
225229
if (hasMixedContent || this.isDynamic) {
226230
this.textStorage.reload("");
231+
this.realpath = this.path;
227232
}
228233
this.scriptKind = scriptKind
229234
? scriptKind
@@ -264,6 +269,30 @@ namespace ts.server {
264269
return this.textStorage.getSnapshot();
265270
}
266271

272+
private ensureRealPath() {
273+
if (this.realpath === undefined) {
274+
// Default is just the path
275+
this.realpath = this.path;
276+
if (this.host.realpath) {
277+
Debug.assert(!!this.containingProjects.length);
278+
const project = this.containingProjects[0];
279+
const realpath = this.host.realpath(this.path);
280+
if (realpath) {
281+
this.realpath = project.toPath(realpath);
282+
// If it is different from this.path, add to the map
283+
if (this.realpath !== this.path) {
284+
project.projectService.realpathToScriptInfos.add(this.realpath, this);
285+
}
286+
}
287+
}
288+
}
289+
}
290+
291+
/*@internal*/
292+
getRealpathIfDifferent(): Path | undefined {
293+
return this.realpath && this.realpath !== this.path ? this.realpath : undefined;
294+
}
295+
267296
getFormatCodeSettings() {
268297
return this.formatCodeSettings;
269298
}
@@ -272,6 +301,9 @@ namespace ts.server {
272301
const isNew = !this.isAttached(project);
273302
if (isNew) {
274303
this.containingProjects.push(project);
304+
if (!project.getCompilerOptions().preserveSymlinks) {
305+
this.ensureRealPath();
306+
}
275307
}
276308
return isNew;
277309
}

0 commit comments

Comments
 (0)