From ef7f13139838101e305a716b3fe1bdeba9a1804f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 11 Jan 2018 15:45:05 -0800 Subject: [PATCH] Add test case for symLink and rename --- .../unittests/tsserverProjectSystem.ts | 107 ++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 139 +++++++++++++++--- 2 files changed, 224 insertions(+), 22 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fe7a2095b86..2811584bf1b 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -6560,4 +6560,111 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file.path]); }); }); + + describe("tsserverProjectSystem with symLinks", () => { + it("rename in common file renames all project", () => { + const projects = "/users/username/projects"; + const folderA = `${projects}/a`; + const aFile: FileOrFolder = { + path: `${folderA}/a.ts`, + content: `import {C} from "./c/fc"; console.log(C)` + }; + const aTsconfig: FileOrFolder = { + path: `${folderA}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) + }; + const aC: FileOrFolder = { + path: `${folderA}/c`, + symLink: "../c" + }; + const aFc = `${folderA}/c/fc.ts`; + + const folderB = `${projects}/b`; + const bFile: FileOrFolder = { + path: `${folderB}/b.ts`, + content: `import {C} from "./c/fc"; console.log(C)` + }; + const bTsconfig: FileOrFolder = { + path: `${folderB}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "commonjs" } }) + }; + const bC: FileOrFolder = { + path: `${folderB}/c`, + symLink: "../c" + }; + const bFc = `${folderB}/c/fc.ts`; + + const folderC = `${projects}/c`; + const cFile: FileOrFolder = { + path: `${folderC}/fc.ts`, + content: `export const C = 8` + }; + + const files = [cFile, libFile, aFile, aTsconfig, aC, bFile, bTsconfig, bC]; + const host = createServerHost(files); + const session = createSession(host); + const projectService = session.getProjectService(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: aFile.path, + projectRootPath: folderA + } + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: bFile.path, + projectRootPath: folderB + } + }); + + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: aFc, + projectRootPath: folderA + } + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: bFc, + projectRootPath: folderB + } + }); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.isDefined(projectService.configuredProjects.get(aTsconfig.path)); + assert.isDefined(projectService.configuredProjects.get(bTsconfig.path)); + + verifyRenameResponse(session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { + file: aFc, + line: 1, + offset: 14, + findInStrings: false, + findInComments: false + } + }).response as protocol.RenameResponseBody); + + 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 } } + ] + }); + } + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 921d4674231..5b326789247 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -70,6 +70,7 @@ interface Array {}` path: string; content?: string; fileSize?: number; + symLink?: string; } interface FSEntry { @@ -86,6 +87,10 @@ interface Array {}` entries: FSEntry[]; } + interface SymLink extends FSEntry { + symLink: Path; + } + function isFolder(s: FSEntry): s is Folder { return s && isArray((s).entries); } @@ -94,6 +99,10 @@ interface Array {}` return s && isString((s).content); } + function isSymLink(s: FSEntry): s is SymLink { + return s && isString((s).symLink); + } + function invokeWatcherCallbacks(callbacks: T[], invokeCallback: (cb: T) => void): void { if (callbacks) { // The array copy is made to ensure that even if one of the callback removes the callbacks, @@ -316,9 +325,12 @@ interface Array {}` } } else { - // TODO: Changing from file => folder + // TODO: Changing from file => folder/Symlink } } + else if (isSymLink(currentEntry)) { + // TODO: update symlinks + } else { // Folder if (isString(fileOrDirectory.content)) { @@ -339,7 +351,7 @@ interface Array {}` // If this entry is not from the new file or folder if (!mapNewLeaves.get(path)) { // Leaf entries that arent in new list => remove these - if (isFile(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) { + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) { this.removeFileOrFolder(fileOrDirectory, folder => !mapNewLeaves.get(folder.path)); } } @@ -387,6 +399,12 @@ interface Array {}` const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate); } + else if (isString(fileOrDirectory.symLink)) { + const symLink = this.toSymLink(fileOrDirectory); + Debug.assert(!this.fs.get(symLink.path)); + const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath)); + this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate); + } else { const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); this.ensureFolder(fullPath); @@ -414,20 +432,20 @@ interface Array {}` return folder; } - private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder, ignoreWatch?: boolean) { + private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder | SymLink, ignoreWatch?: boolean) { folder.entries.push(fileOrDirectory); this.fs.set(fileOrDirectory.path, fileOrDirectory); if (ignoreWatch) { return; } - if (isFile(fileOrDirectory)) { + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); } this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } - private removeFileOrFolder(fileOrDirectory: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) { + private removeFileOrFolder(fileOrDirectory: File | Folder | SymLink, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) { const basePath = getDirectoryPath(fileOrDirectory.path); const baseFolder = this.fs.get(basePath) as Folder; if (basePath !== fileOrDirectory.path) { @@ -436,7 +454,7 @@ interface Array {}` } this.fs.delete(fileOrDirectory.path); - if (isFile(fileOrDirectory)) { + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); } else { @@ -503,6 +521,15 @@ interface Array {}` }; } + private toSymLink(fileOrDirectory: FileOrFolder): SymLink { + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + return { + path: this.toPath(fullPath), + fullPath, + symLink: this.toPath(getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath))) + }; + } + private toFolder(path: string): Folder { const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); return { @@ -512,14 +539,70 @@ interface Array {}` }; } - fileExists(s: string) { - const path = this.toFullPath(s); - return isFile(this.fs.get(path)); + private isFile(fsEntry: FSEntry) { + return !!this.getRealFile(fsEntry.path, fsEntry); } - readFile(s: string) { - const fsEntry = this.fs.get(this.toFullPath(s)); - return isFile(fsEntry) ? fsEntry.content : undefined; + private getRealFile(path: Path, fsEntry = this.fs.get(path)): File | undefined { + if (isFile(fsEntry)) { + return fsEntry; + } + + if (isSymLink(fsEntry)) { + return this.getRealFile(fsEntry.symLink); + } + + if (fsEntry) { + // This fs entry is something else + 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); + } + return undefined; + } + + 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; + } + + fileExists(s: string) { + const path = this.toFullPath(s); + return !!this.getRealFile(path); + } + + readFile(s: string): string { + const fsEntry = this.getRealFile(this.toFullPath(s)); + return fsEntry ? fsEntry.content : undefined; } getFileSize(s: string) { @@ -533,14 +616,14 @@ interface Array {}` directoryExists(s: string) { const path = this.toFullPath(s); - return isFolder(this.fs.get(path)); + return !!this.getRealFolder(path); } - getDirectories(s: string) { + getDirectories(s: string): string[] { const path = this.toFullPath(s); - const folder = this.fs.get(path); - if (isFolder(folder)) { - return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); + const folder = this.getRealFolder(path); + if (folder) { + return mapDefined(folder.entries, entry => this.isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); } Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); return []; @@ -550,13 +633,13 @@ interface Array {}` return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { const directories: string[] = []; const files: string[] = []; - const dirEntry = this.fs.get(this.toPath(dir)); - if (isFolder(dirEntry)) { - dirEntry.entries.forEach((entry) => { - if (isFolder(entry)) { + const folder = this.getRealFolder(this.toPath(dir)); + if (folder) { + folder.entries.forEach((entry) => { + if (this.isFolder(entry)) { directories.push(getBaseFileName(entry.fullPath)); } - else if (isFile(entry)) { + else if (this.isFile(entry)) { files.push(getBaseFileName(entry.fullPath)); } else { @@ -682,6 +765,18 @@ interface Array {}` 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; + } + } + } + readonly existMessage = "System Exit"; exitCode: number; readonly resolvePath = (s: string) => s;