diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 4bd4801ce8f..112eca82926 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1173,6 +1173,21 @@ namespace ts { }}; } + export function arrayReverseIterator(array: ReadonlyArray): Iterator { + let i = array.length; + return { + next: () => { + if (i === 0) { + return { value: undefined as never, done: true }; + } + else { + i--; + return { value: array[i], done: false }; + } + } + }; + } + /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. */ diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e9d97f6e03c..ed51028392e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -407,6 +407,21 @@ namespace ts.server { } } + /*@internal*/ + export interface OpenFileArguments { + fileName: string; + content?: string; + scriptKind?: protocol.ScriptKindName | ScriptKind; + hasMixedContent?: boolean; + projectRootPath?: string; + } + + /*@internal*/ + export interface ChangeFileArguments { + fileName: string; + changes: Iterator; + } + export class ProjectService { /*@internal*/ @@ -2770,18 +2785,22 @@ namespace ts.server { } /* @internal */ - applyChangesInOpenFiles(openFiles: protocol.ExternalFile[] | undefined, changedFiles: protocol.ChangedOpenFile[] | undefined, closedFiles: string[] | undefined): void { + applyChangesInOpenFiles(openFiles: Iterator | undefined, changedFiles?: Iterator, closedFiles?: string[]): void { if (openFiles) { - for (const file of openFiles) { + while (true) { + const { value: file, done } = openFiles.next(); + if (done) break; const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!scriptInfo || !scriptInfo.isScriptOpen(), "Script should not exist and not be open already"); const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName); - this.openClientFileWithNormalizedPath(normalizedPath, file.content, tryConvertScriptKindName(file.scriptKind!), file.hasMixedContent); // TODO: GH#18217 + this.openClientFileWithNormalizedPath(normalizedPath, file.content, tryConvertScriptKindName(file.scriptKind!), file.hasMixedContent, file.projectRootPath ? toNormalizedPath(file.projectRootPath) : undefined); // TODO: GH#18217 } } if (changedFiles) { - for (const file of changedFiles) { + while (true) { + const { value: file, done } = changedFiles.next(); + if (done) break; const scriptInfo = this.getScriptInfo(file.fileName)!; Debug.assert(!!scriptInfo); this.applyChangesToFile(scriptInfo, file.changes); @@ -2796,10 +2815,10 @@ namespace ts.server { } /* @internal */ - applyChangesToFile(scriptInfo: ScriptInfo, changes: TextChange[]) { - // apply changes in reverse order - for (let i = changes.length - 1; i >= 0; i--) { - const change = changes[i]; + applyChangesToFile(scriptInfo: ScriptInfo, changes: Iterator) { + while (true) { + const { value: change, done } = changes.next(); + if (done) break; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1fd8d98b570..3534a991c48 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -92,6 +92,7 @@ namespace ts.server.protocol { SynchronizeProjectList = "synchronizeProjectList", /* @internal */ ApplyChangedToOpenFiles = "applyChangedToOpenFiles", + ApplyChangesToOpenFiles = "applyChangesToOpenFiles", /* @internal */ EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full", /* @internal */ @@ -1543,6 +1544,32 @@ namespace ts.server.protocol { closedFiles?: string[]; } + /** + * Request to synchronize list of open files with the client + */ + export interface ApplyChangesToOpenFilesRequest extends Request { + command: CommandTypes.ApplyChangesToOpenFiles; + arguments: ApplyChangesToOpenFilesRequestArgs; + } + + /** + * Arguments to ApplyChangesToOpenFilesRequest + */ + export interface ApplyChangesToOpenFilesRequestArgs { + /** + * List of newly open files + */ + openFiles?: OpenRequestArgs[]; + /** + * List of open files files that were changes + */ + changedFiles?: FileCodeEdits[]; + /** + * List of files that were closed + */ + closedFiles?: string[]; + } + /** * Request to set compiler options for inferred projects. * External projects are opened / closed explicitly. diff --git a/src/server/session.ts b/src/server/session.ts index b47dfe6b716..42946002bc4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1654,10 +1654,10 @@ namespace ts.server { const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { this.changeSeq++; - this.projectService.applyChangesToFile(scriptInfo, [{ + this.projectService.applyChangesToFile(scriptInfo, singleIterator({ span: { start, length: end - start }, newText: args.insertString! // TODO: GH#18217 - }]); + })); } } @@ -2096,9 +2096,39 @@ namespace ts.server { }); return this.requiredResponse(converted); }, + [CommandNames.ApplyChangesToOpenFiles]: (request: protocol.ApplyChangesToOpenFilesRequest) => { + this.changeSeq++; + this.projectService.applyChangesInOpenFiles( + request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), file => ({ + fileName: file.file, + content: file.fileContent, + scriptKind: file.scriptKindName, + projectRootPath: file.projectRootPath + })), + request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({ + fileName: file.fileName, + changes: mapDefinedIterator(arrayIterator(file.textChanges), change => { + const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file.fileName)); + const start = scriptInfo.lineOffsetToPosition(change.start.line, change.start.offset); + const end = scriptInfo.lineOffsetToPosition(change.end.line, change.end.offset); + return start >= 0 ? { span: { start, length: end - start }, newText: change.newText } : undefined; + }) + })), + request.arguments.closedFiles + ); + return this.requiredResponse(/*response*/ true); + }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { this.changeSeq++; - this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles!, request.arguments.closedFiles!); // TODO: GH#18217 + this.projectService.applyChangesInOpenFiles( + request.arguments.openFiles && arrayIterator(request.arguments.openFiles), + request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({ + fileName: file.fileName, + // apply changes in reverse order + changes: arrayReverseIterator(file.changes) + })), + request.arguments.closedFiles + ); // TODO: report errors return this.requiredResponse(/*response*/ true); }, diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index f964e77e0eb..e5dd2e58754 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -31,7 +31,7 @@ ${file.content}`; function verify(applyChangesToOpen: (session: TestSession) => void) { const host = createServerHost([app, file3, commonFile1, commonFile2, libFile, configFile]); - const session = projectSystem.createSession(host); + const session = createSession(host); session.executeCommandSeq({ command: protocol.CommandTypes.Open, arguments: { file: app.path } @@ -104,5 +104,45 @@ ${file.content}`; }) ); }); + + it("with applyChangesToOpenFiles request", () => { + verify(session => + session.executeCommandSeq({ + command: protocol.CommandTypes.ApplyChangesToOpenFiles, + arguments: { + openFiles: [ + { + file: commonFile1.path, + fileContent: fileContentWithComment(commonFile1) + }, + { + file: commonFile2.path, + fileContent: fileContentWithComment(commonFile2) + } + ], + changedFiles: [ + { + fileName: app.path, + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zz = 10;", + }, + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zzz = 10;", + } + ] + } + ], + closedFiles: [ + file3.path + ] + } + }) + ); + }); }); } diff --git a/src/testRunner/unittests/tsserver/documentRegistry.ts b/src/testRunner/unittests/tsserver/documentRegistry.ts index 1761e413833..10723300cc0 100644 --- a/src/testRunner/unittests/tsserver/documentRegistry.ts +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -41,13 +41,13 @@ namespace ts.projectSystem { function changeFileToNotImportModule(service: TestProjectService) { const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]); + service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: importModuleContent.length }, newText: "" })); checkProject(service, /*moduleIsOrphan*/ true); } function changeFileToImportModule(service: TestProjectService) { const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]); + service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: 0 }, newText: importModuleContent })); checkProject(service, /*moduleIsOrphan*/ false); } diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 2055141538a..82c706500d3 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -161,7 +161,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 0); externalFiles[0].content = "let x =1;"; - projectService.applyChangesInOpenFiles(externalFiles, [], []); + projectService.applyChangesInOpenFiles(arrayIterator(externalFiles)); }); it("external project that included config files", () => { @@ -790,9 +790,7 @@ namespace ts.projectSystem { rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], options: { allowJs: false } }]); - service.applyChangesInOpenFiles([ - { fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" } - ], /*changedFiles*/ undefined, /*closedFiles*/ undefined); + service.applyChangesInOpenFiles(singleIterator({ fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" })); checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); checkProjectActualFiles(configProject, [tsconfig.path]); const inferredProject = service.inferredProjects[0]; diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 3a648c9189e..72eff18f5ba 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -202,7 +202,7 @@ namespace ts.projectSystem { const host = createServerHost([file1, config1]); const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); - projectService.applyChangesInOpenFiles([{ fileName: file1.path, content: file1.content }], [], []); + projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content })); checkNumberOfProjects(projectService, { inferredProjects: 1 }); const proj = projectService.inferredProjects[0]; @@ -588,11 +588,11 @@ namespace ts.projectSystem { const host = createServerHost([]); const projectService = createProjectService(host); - projectService.applyChangesInOpenFiles([tsFile], [], []); + projectService.applyChangesInOpenFiles(singleIterator(tsFile)); const projs = projectService.synchronizeProjectList([]); projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); projectService.synchronizeProjectList([projs[0].info!]); - projectService.applyChangesInOpenFiles([jsFile], [], []); + projectService.applyChangesInOpenFiles(singleIterator(jsFile)); }); it("config file is deleted", () => { @@ -696,11 +696,12 @@ namespace ts.projectSystem { checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Open HTML file - projectService.applyChangesInOpenFiles( - /*openFiles*/[{ fileName: file2.path, hasMixedContent: true, scriptKind: ScriptKind.JS, content: `var hello = "hello";` }], - /*changedFiles*/ undefined, - /*closedFiles*/ undefined); - + projectService.applyChangesInOpenFiles(singleIterator({ + fileName: file2.path, + hasMixedContent: true, + scriptKind: ScriptKind.JS, + content: `var hello = "hello";` + })); // Now HTML file is included in the project checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); @@ -852,7 +853,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); projectService.applyChangesInOpenFiles( /*openFiles*/ undefined, - /*changedFiles*/[{ fileName: file1.path, changes: [{ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }] }], + /*changedFiles*/singleIterator({ fileName: file1.path, changes: singleIterator({ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }) }), /*closedFiles*/ undefined); checkNumberOfProjects(projectService, { inferredProjects: 1 }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index fab6f905efc..eb522125ddd 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5720,6 +5720,7 @@ declare namespace ts.server.protocol { OpenExternalProject = "openExternalProject", OpenExternalProjects = "openExternalProjects", CloseExternalProject = "closeExternalProject", + ApplyChangesToOpenFiles = "applyChangesToOpenFiles", GetOutliningSpans = "getOutliningSpans", TodoComments = "todoComments", Indentation = "indentation", @@ -6788,6 +6789,30 @@ declare namespace ts.server.protocol { */ interface CloseExternalProjectResponse extends Response { } + /** + * Request to synchronize list of open files with the client + */ + interface ApplyChangesToOpenFilesRequest extends Request { + command: CommandTypes.ApplyChangesToOpenFiles; + arguments: ApplyChangesToOpenFilesRequestArgs; + } + /** + * Arguments to ApplyChangesToOpenFilesRequest + */ + interface ApplyChangesToOpenFilesRequestArgs { + /** + * List of newly open files + */ + openFiles?: OpenRequestArgs[]; + /** + * List of open files files that were changes + */ + changedFiles?: FileCodeEdits[]; + /** + * List of files that were closed + */ + closedFiles?: string[]; + } /** * Request to set compiler options for inferred projects. * External projects are opened / closed explicitly.