Skip to content

Commit 9ad9dc1

Browse files
authored
Merge pull request #21171 from Microsoft/renameSymLinks
Rename through all projects with same file through symLink
2 parents 41c02e6 + 428e052 commit 9ad9dc1

File tree

7 files changed

+389
-62
lines changed

7 files changed

+389
-62
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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6616,4 +6616,118 @@ namespace ts.projectSystem {
66166616
checkProjectActualFiles(project, [file.path]);
66176617
});
66186618
});
6619+
6620+
describe("tsserverProjectSystem with symLinks", () => {
6621+
it("rename in common file renames all project", () => {
6622+
const projects = "/users/username/projects";
6623+
const folderA = `${projects}/a`;
6624+
const aFile: FileOrFolder = {
6625+
path: `${folderA}/a.ts`,
6626+
content: `import {C} from "./c/fc"; console.log(C)`
6627+
};
6628+
const aTsconfig: FileOrFolder = {
6629+
path: `${folderA}/tsconfig.json`,
6630+
content: JSON.stringify({ compilerOptions: { module: "commonjs" } })
6631+
};
6632+
const aC: FileOrFolder = {
6633+
path: `${folderA}/c`,
6634+
symLink: "../c"
6635+
};
6636+
const aFc = `${folderA}/c/fc.ts`;
6637+
6638+
const folderB = `${projects}/b`;
6639+
const bFile: FileOrFolder = {
6640+
path: `${folderB}/b.ts`,
6641+
content: `import {C} from "./c/fc"; console.log(C)`
6642+
};
6643+
const bTsconfig: FileOrFolder = {
6644+
path: `${folderB}/tsconfig.json`,
6645+
content: JSON.stringify({ compilerOptions: { module: "commonjs" } })
6646+
};
6647+
const bC: FileOrFolder = {
6648+
path: `${folderB}/c`,
6649+
symLink: "../c"
6650+
};
6651+
const bFc = `${folderB}/c/fc.ts`;
6652+
6653+
const folderC = `${projects}/c`;
6654+
const cFile: FileOrFolder = {
6655+
path: `${folderC}/fc.ts`,
6656+
content: `export const C = 8`
6657+
};
6658+
6659+
const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC];
6660+
const host = createServerHost(files);
6661+
const session = createSession(host);
6662+
const projectService = session.getProjectService();
6663+
debugger;
6664+
session.executeCommandSeq<protocol.OpenRequest>({
6665+
command: protocol.CommandTypes.Open,
6666+
arguments: {
6667+
file: aFile.path,
6668+
projectRootPath: folderA
6669+
}
6670+
});
6671+
session.executeCommandSeq<protocol.OpenRequest>({
6672+
command: protocol.CommandTypes.Open,
6673+
arguments: {
6674+
file: bFile.path,
6675+
projectRootPath: folderB
6676+
}
6677+
});
6678+
6679+
session.executeCommandSeq<protocol.OpenRequest>({
6680+
command: protocol.CommandTypes.Open,
6681+
arguments: {
6682+
file: aFc,
6683+
projectRootPath: folderA
6684+
}
6685+
});
6686+
session.executeCommandSeq<protocol.OpenRequest>({
6687+
command: protocol.CommandTypes.Open,
6688+
arguments: {
6689+
file: bFc,
6690+
projectRootPath: folderB
6691+
}
6692+
});
6693+
checkNumberOfProjects(projectService, { configuredProjects: 2 });
6694+
assert.isDefined(projectService.configuredProjects.get(aTsconfig.path));
6695+
assert.isDefined(projectService.configuredProjects.get(bTsconfig.path));
6696+
6697+
debugger;
6698+
verifyRenameResponse(session.executeCommandSeq<protocol.RenameRequest>({
6699+
command: protocol.CommandTypes.Rename,
6700+
arguments: {
6701+
file: aFc,
6702+
line: 1,
6703+
offset: 14,
6704+
findInStrings: false,
6705+
findInComments: false
6706+
}
6707+
}).response as protocol.RenameResponseBody);
6708+
6709+
function verifyRenameResponse({ info, locs }: protocol.RenameResponseBody) {
6710+
assert.isTrue(info.canRename);
6711+
assert.equal(locs.length, 4);
6712+
verifyLocations(0, aFile.path, aFc);
6713+
verifyLocations(2, bFile.path, bFc);
6714+
6715+
function verifyLocations(locStartIndex: number, firstFile: string, secondFile: string) {
6716+
assert.deepEqual(locs[locStartIndex], {
6717+
file: firstFile,
6718+
locs: [
6719+
{ start: { line: 1, offset: 39 }, end: { line: 1, offset: 40 } },
6720+
{ start: { line: 1, offset: 9 }, end: { line: 1, offset: 10 } }
6721+
]
6722+
});
6723+
assert.deepEqual(locs[locStartIndex + 1], {
6724+
file: secondFile,
6725+
locs: [
6726+
{ start: { line: 1, offset: 14 }, end: { line: 1, offset: 15 } }
6727+
]
6728+
});
6729+
}
6730+
}
6731+
});
6732+
});
66196733
}

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ interface Array<T> {}`
7070
path: string;
7171
content?: string;
7272
fileSize?: number;
73+
symLink?: string;
7374
}
7475

7576
interface FSEntry {
@@ -86,6 +87,10 @@ interface Array<T> {}`
8687
entries: FSEntry[];
8788
}
8889

90+
interface SymLink extends FSEntry {
91+
symLink: string;
92+
}
93+
8994
function isFolder(s: FSEntry): s is Folder {
9095
return s && isArray((<Folder>s).entries);
9196
}
@@ -94,6 +99,10 @@ interface Array<T> {}`
9499
return s && isString((<File>s).content);
95100
}
96101

102+
function isSymLink(s: FSEntry): s is SymLink {
103+
return s && isString((<SymLink>s).symLink);
104+
}
105+
97106
function invokeWatcherCallbacks<T>(callbacks: T[], invokeCallback: (cb: T) => void): void {
98107
if (callbacks) {
99108
// The array copy is made to ensure that even if one of the callback removes the callbacks,
@@ -316,9 +325,12 @@ interface Array<T> {}`
316325
}
317326
}
318327
else {
319-
// TODO: Changing from file => folder
328+
// TODO: Changing from file => folder/Symlink
320329
}
321330
}
331+
else if (isSymLink(currentEntry)) {
332+
// TODO: update symlinks
333+
}
322334
else {
323335
// Folder
324336
if (isString(fileOrDirectory.content)) {
@@ -339,7 +351,7 @@ interface Array<T> {}`
339351
// If this entry is not from the new file or folder
340352
if (!mapNewLeaves.get(path)) {
341353
// Leaf entries that arent in new list => remove these
342-
if (isFile(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) {
354+
if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) {
343355
this.removeFileOrFolder(fileOrDirectory, folder => !mapNewLeaves.get(folder.path));
344356
}
345357
}
@@ -387,6 +399,12 @@ interface Array<T> {}`
387399
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath));
388400
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
389401
}
402+
else if (isString(fileOrDirectory.symLink)) {
403+
const symLink = this.toSymLink(fileOrDirectory);
404+
Debug.assert(!this.fs.get(symLink.path));
405+
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath));
406+
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate);
407+
}
390408
else {
391409
const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory);
392410
this.ensureFolder(fullPath);
@@ -414,20 +432,20 @@ interface Array<T> {}`
414432
return folder;
415433
}
416434

417-
private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder, ignoreWatch?: boolean) {
435+
private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder | SymLink, ignoreWatch?: boolean) {
418436
folder.entries.push(fileOrDirectory);
419437
this.fs.set(fileOrDirectory.path, fileOrDirectory);
420438

421439
if (ignoreWatch) {
422440
return;
423441
}
424-
if (isFile(fileOrDirectory)) {
442+
if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) {
425443
this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created);
426444
}
427445
this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath);
428446
}
429447

430-
private removeFileOrFolder(fileOrDirectory: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) {
448+
private removeFileOrFolder(fileOrDirectory: File | Folder | SymLink, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) {
431449
const basePath = getDirectoryPath(fileOrDirectory.path);
432450
const baseFolder = this.fs.get(basePath) as Folder;
433451
if (basePath !== fileOrDirectory.path) {
@@ -436,7 +454,7 @@ interface Array<T> {}`
436454
}
437455
this.fs.delete(fileOrDirectory.path);
438456

439-
if (isFile(fileOrDirectory)) {
457+
if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) {
440458
this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted);
441459
}
442460
else {
@@ -503,6 +521,15 @@ interface Array<T> {}`
503521
};
504522
}
505523

524+
private toSymLink(fileOrDirectory: FileOrFolder): SymLink {
525+
const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory);
526+
return {
527+
path: this.toPath(fullPath),
528+
fullPath,
529+
symLink: getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath))
530+
};
531+
}
532+
506533
private toFolder(path: string): Folder {
507534
const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
508535
return {
@@ -512,14 +539,52 @@ interface Array<T> {}`
512539
};
513540
}
514541

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)) {
544+
return fsEntry;
545+
}
546+
547+
if (isSymLink(fsEntry)) {
548+
return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink));
549+
}
550+
551+
if (fsEntry) {
552+
// This fs entry is something else
553+
return undefined;
554+
}
555+
556+
const realpath = this.realpath(path);
557+
if (path !== realpath) {
558+
return this.getRealFsEntry(isFsEntry, realpath as Path);
559+
}
560+
561+
return undefined;
562+
}
563+
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+
572+
private isFolder(fsEntry: FSEntry) {
573+
return !!this.getRealFolder(fsEntry.path, fsEntry);
574+
}
575+
576+
private getRealFolder(path: Path, fsEntry = this.fs.get(path)): Folder | undefined {
577+
return this.getRealFsEntry(isFolder, path, fsEntry);
578+
}
579+
515580
fileExists(s: string) {
516581
const path = this.toFullPath(s);
517-
return isFile(this.fs.get(path));
582+
return !!this.getRealFile(path);
518583
}
519584

520-
readFile(s: string) {
521-
const fsEntry = this.fs.get(this.toFullPath(s));
522-
return isFile(fsEntry) ? fsEntry.content : undefined;
585+
readFile(s: string): string {
586+
const fsEntry = this.getRealFile(this.toFullPath(s));
587+
return fsEntry ? fsEntry.content : undefined;
523588
}
524589

525590
getFileSize(s: string) {
@@ -533,14 +598,14 @@ interface Array<T> {}`
533598

534599
directoryExists(s: string) {
535600
const path = this.toFullPath(s);
536-
return isFolder(this.fs.get(path));
601+
return !!this.getRealFolder(path);
537602
}
538603

539-
getDirectories(s: string) {
604+
getDirectories(s: string): string[] {
540605
const path = this.toFullPath(s);
541-
const folder = this.fs.get(path);
542-
if (isFolder(folder)) {
543-
return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
606+
const folder = this.getRealFolder(path);
607+
if (folder) {
608+
return mapDefined(folder.entries, entry => this.isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
544609
}
545610
Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
546611
return [];
@@ -550,13 +615,13 @@ interface Array<T> {}`
550615
return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
551616
const directories: string[] = [];
552617
const files: string[] = [];
553-
const dirEntry = this.fs.get(this.toPath(dir));
554-
if (isFolder(dirEntry)) {
555-
dirEntry.entries.forEach((entry) => {
556-
if (isFolder(entry)) {
618+
const folder = this.getRealFolder(this.toPath(dir));
619+
if (folder) {
620+
folder.entries.forEach((entry) => {
621+
if (this.isFolder(entry)) {
557622
directories.push(getBaseFileName(entry.fullPath));
558623
}
559-
else if (isFile(entry)) {
624+
else if (this.isFile(entry)) {
560625
files.push(getBaseFileName(entry.fullPath));
561626
}
562627
else {
@@ -682,6 +747,23 @@ interface Array<T> {}`
682747
clear(this.output);
683748
}
684749

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);
762+
}
763+
764+
return realFullPath;
765+
}
766+
685767
readonly existMessage = "System Exit";
686768
exitCode: number;
687769
readonly resolvePath = (s: string) => s;

0 commit comments

Comments
 (0)