diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 01cdbee8091..c5c099ce161 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -424,16 +424,6 @@ namespace ts.server { script.svc.reloadFromFile(tmpfilename, cb); } } - - editScript(filename: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(filename); - if (script) { - script.editContent(start, end, newText); - return; - } - - throw new Error("No script with name '" + filename + "'"); - } } class InferredProject extends Project { @@ -453,21 +443,19 @@ namespace ts.server { } } - interface Delta { - addedFiles: string[]; - removedFiles: string[]; - replacedFiles: string[]; - projectName: string; - version: number; + function findVersionedProjectByFileName(projectFileName: string, projects: T[]): T { + for (const proj of projects) { + if (proj.getProjectFileName() === projectFileName) { + return proj; + } + } } - const id = (x: any) => x; - abstract class VersionedProject extends Project { private lastReportedFileNames: Map; private lastReportedVersion: number = 0; - private currentVersion: number = 1; + currentVersion: number = 1; updateGraph() { const oldProgram = this.program; @@ -479,55 +467,41 @@ namespace ts.server { } } - getDeltaFromVersion(lastKnownVersion?: number): Delta { + getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles { + const info = { + projectFileName: this.getProjectFileName(), + version: this.currentVersion + }; if (this.lastReportedVersion === this.currentVersion) { - return { - projectName: this.getProjectFileName(), - addedFiles: [], - removedFiles: [], - version: this.currentVersion, - replacedFiles: [] - }; + return { info }; } if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) { const lastReportedFileNames = this.lastReportedFileNames; - const currentFiles = arrayToMap(this.getFileNames(), id); + const currentFiles = arrayToMap(this.getFileNames(), x => x); - const addedFiles: string[] = []; - const removedFiles: string[] = []; + const added: string[] = []; + const removed: string[] = []; for (const id in currentFiles) { if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) { - addedFiles.push(id); + added.push(id); } } for (const id in lastReportedFileNames) { if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) { - removedFiles.push(id); + removed.push(id); } } this.lastReportedFileNames = currentFiles; this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.currentVersion; - return { - projectName: this.getProjectFileName(), - addedFiles, - removedFiles, - version: this.currentVersion, - replacedFiles: [] - }; + return { info, changes: { added, removed } }; } else { // unknown version - return everything const projectFileNames = this.getFileNames(); - this.lastReportedFileNames = arrayToMap(projectFileNames, id); - return { - projectName: this.getProjectFileName(), - addedFiles: [], - removedFiles: [], - version: this.currentVersion, - replacedFiles: projectFileNames - }; + this.lastReportedFileNames = arrayToMap(projectFileNames, x => x); + return { info, files: projectFileNames }; } } } @@ -627,6 +601,11 @@ namespace ts.server { **/ openFileRoots: ScriptInfo[] = []; + /** + * maps external project file name to list of config files that were the part of this project + */ + externalProjectToConfiguredProjectMap: ts.Map = {}; + /** * external projects (configuration and list of root files is not controlled by tsserver) */ @@ -1078,12 +1057,11 @@ namespace ts.server { } private findConfiguredProjectByConfigFile(configFileName: string) { - for (const configuredProject of this.configuredProjects) { - if (configuredProject.configFileName === configFileName) { - return configuredProject; - } - } - return undefined; + return findVersionedProjectByFileName(configFileName, this.configuredProjects); + } + + private findExternalProjectByProjectFileName(projectFileName: string) { + return findVersionedProjectByFileName(projectFileName, this.externalProjects); } private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { @@ -1284,13 +1262,6 @@ namespace ts.server { return info; } - openExternalProject(proj: protocol.ExternalProject) { - } - - closeExternalProject(proj: protocol.ExternalProject) { - // TODO: save mapping from external project name to set of configured projects - } - log(msg: string, type = "Err") { this.psLogger.msg(msg, type); } @@ -1475,11 +1446,70 @@ namespace ts.server { } } - loadExternalProject(externalProject: protocol.ExternalProject): Delta[] { + private addExternalProjectFilesForVersionedProjects(knownProjects: protocol.ExternalProjectInfo[], projects: VersionedProject[], result: protocol.ExternalProjectFiles[]): void { + for (const proj of projects) { + const knownProject = ts.forEach(knownProjects, p => p.projectFileName === proj.getProjectFileName() && p); + result.push(proj.getChangesSinceVersion(knownProjects && knownProject.version)); + } + } + + synchronizeProjectList(knownProjects: protocol.ExternalProjectInfo[]): protocol.ExternalProjectFiles[] { + const files: protocol.ExternalProjectFiles[] = []; + this.addExternalProjectFilesForVersionedProjects(knownProjects, this.externalProjects, files); + this.addExternalProjectFilesForVersionedProjects(knownProjects, this.configuredProjects, files); + for (const inferredProject of this.inferredProjects) { + files.push({ files: inferredProject.getFileNames() }); + } + return files; + } + + applyChangesInOpenFiles(openFiles: protocol.OpenFile[], closedFiles: string[]): void { + for (const file of openFiles) { + const scriptInfo = this.getScriptInfo(file.fileName); + if (!scriptInfo) { + Debug.assert(!!file.content); + this.openClientFile(file.fileName, file.content); + } + else { + Debug.assert(!!file.textChanges); + for (const change of file.textChanges) { + scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); + } + } + } + + for (const file of closedFiles) { + this.closeClientFile(file); + } + + this.updateProjectStructure(); + } + + closeExternalProject(fileName: string): void { + fileName = normalizePath(fileName); + const configFiles = this.externalProjectToConfiguredProjectMap[fileName]; + if (configFiles) { + for (const configFile of configFiles) { + const configuredProject = this.findConfiguredProjectByConfigFile(configFile); + if (configuredProject) { + this.removeProject(configuredProject); + } + } + } + else { + // close external project + const externalProject = this.findExternalProjectByProjectFileName(fileName); + if (externalProject) { + this.removeProject(externalProject); + } + } + this.updateProjectStructure(); + } + + openExternalProject(externalProject: protocol.ExternalProject): void { const project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); if (project) { this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); - return [project.getDeltaFromVersion()]; } else { let tsConfigFiles: string[]; @@ -1493,32 +1523,18 @@ namespace ts.server { } } if (tsConfigFiles) { - const deltas: Delta[] = []; for (const tsconfigFile of tsConfigFiles) { const { success, project, errors } = this.openConfigFile(tsconfigFile); if (success) { // keep project alive project.addOpenRef(); - deltas.push(project.getDeltaFromVersion()); } } - return deltas; } else { - const { project, errors } = this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); - return [project.getDeltaFromVersion()]; + this.createAndAddExternalProject(externalProject.projectFileName, externalProject.rootFiles, externalProject.options); } } } - - loadExternalProjects(externalProjects: protocol.ExternalProject[], openFiles: protocol.OpenFile[]): void { - for (const project of externalProjects) { - this.loadExternalProject(project); - } - for (const openFile of openFiles) { - this.getOrCreateScriptInfo(openFile.fileName, /*openedByClient*/ true, openFile.content); - } - // TODO: return diff - } } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 4702265e663..e04c033398b 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -424,9 +424,37 @@ declare namespace ts.server.protocol { options: CompilerOptions; } + export interface ExternalProjectInfo { + projectFileName: string; + version: number; + } + + export interface ExternalProjectChanges { + added: string[]; + removed: string[]; + } + + /** + * Describes set of files in the project. + * info might be omitted in case of inferred projects + * if files is set - then this is the entire set of files in the project + * if changes is set - then this is the set of changes that should be applied to existing project + * otherwise - assume that nothing is changed + */ + export interface ExternalProjectFiles { + info?: ExternalProjectInfo; + files?: string[]; + changes?: ExternalProjectChanges; + } + + /** + * Represents a set of changes for open document with a given file name. + * Either content of textChanges should be present. + */ export interface OpenFile { fileName: string; content?: string; + textChanges?: ts.TextChange[]; } /** @@ -548,14 +576,35 @@ declare namespace ts.server.protocol { arguments: OpenRequestArgs; } - type LoadExternalProjectArgs = ExternalProject; + type OpenExternalProjectArgs = ExternalProject; - export interface LoadExternalProject extends Request { - arguments: LoadExternalProjectArgs; + export interface OpenExternalProjectRequest extends Request { + arguments: OpenExternalProjectArgs; } - interface LoadExternalProjectResponse extends Response { - files: string[]; + export interface CloseExternalProjectRequestArgs { + projectFileName: string; + } + + export interface CloseExternalProjectRequest extends Request { + arguments: CloseExternalProjectRequestArgs; + } + + export interface SynchronizeProjectListRequest extends Request { + arguments: SynchronizeProjectListRequestArgs; + } + + export interface SynchronizeProjectListRequestArgs { + knownProjects: protocol.ExternalProjectInfo[]; + } + + export interface ApplyChangedToOpenFilesRequest extends Request { + arguments: ApplyChangedToOpenFilesRequestArgs; + } + + export interface ApplyChangedToOpenFilesRequestArgs { + openFiles: OpenFile[]; + closedFiles: string[]; } /** diff --git a/src/server/session.ts b/src/server/session.ts index c7f3a08dea0..a424d94f2db 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -127,7 +127,10 @@ namespace ts.server { export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; - export const LoadExternalProject = "loadExternalProject"; + export const OpenExternalProject = "openExternalProject"; + export const CloseExternalProject = "closeExternalProject"; + export const SynchronizeProjectList = "synchronizeProjectList"; + export const ApplyChangedToOpenFiles = "applyChangedToOpenFiles"; } namespace Errors { @@ -268,7 +271,7 @@ namespace ts.server { } private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { - setTimeout(() => { + this.host.setTimeout(() => { if (matchSeq(seq)) { this.projectService.updateProjectStructure(); } @@ -298,7 +301,7 @@ namespace ts.server { this.semanticCheck(checkSpec.fileName, checkSpec.project); this.immediateId = undefined; if (checkList.length > index) { - this.errorTimer = setTimeout(checkOne, followMs); + this.errorTimer = this.host.setTimeout(checkOne, followMs); } else { this.errorTimer = undefined; @@ -308,7 +311,7 @@ namespace ts.server { } }; if ((checkList.length > index) && (matchSeq(seq))) { - this.errorTimer = setTimeout(checkOne, ms); + this.errorTimer = this.host.setTimeout(checkOne, ms); } } @@ -1028,30 +1031,50 @@ namespace ts.server { exit() { } + private notRequired() { + return { responseRequired: false }; + } + + private requiredResponse(response: any) { + return { response, responseRequired: true }; + } + private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { - [CommandNames.LoadExternalProject]: (request: protocol.Request) => { - const deltas = this.projectService.loadExternalProject(request.arguments); - return { responseRequired: true, response: { files: deltas } }; + [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { + const deltas = this.projectService.openExternalProject(request.arguments); + return this.notRequired(); + }, + [CommandNames.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => { + this.projectService.closeExternalProject(request.arguments.projectFileName); + return this.notRequired(); + }, + [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { + const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects); + return this.requiredResponse(result); + }, + [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { + this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.closedFiles); + return this.notRequired(); }, [CommandNames.Exit]: () => { this.exit(); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Definition]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getDefinition(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.TypeDefinition]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.References]: (request: protocol.Request) => { const defArgs = request.arguments; - return { response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; + return this.requiredResponse(this.getReferences(defArgs.line, defArgs.offset, defArgs.file)); }, [CommandNames.Rename]: (request: protocol.Request) => { const renameArgs = request.arguments; - return { response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true }; + return this.requiredResponse(this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings)); }, [CommandNames.Open]: (request: protocol.Request) => { const openArgs = request.arguments; @@ -1071,7 +1094,7 @@ namespace ts.server { break; } this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); - return { responseRequired: false }; + return this.notRequired(); }, [CommandNames.Quickinfo]: (request: protocol.Request) => { const quickinfoArgs = request.arguments;