Skip to content

Commit d62c8a4

Browse files
authored
Merge pull request #30107 from Microsoft/applyChangesToOpenFiles
Add UpdateOpen to request
2 parents d201320 + e55fbff commit d62c8a4

File tree

10 files changed

+368
-51
lines changed

10 files changed

+368
-51
lines changed

src/compiler/core.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,21 @@ namespace ts {
11731173
}};
11741174
}
11751175

1176+
export function arrayReverseIterator<T>(array: ReadonlyArray<T>): Iterator<T> {
1177+
let i = array.length;
1178+
return {
1179+
next: () => {
1180+
if (i === 0) {
1181+
return { value: undefined as never, done: true };
1182+
}
1183+
else {
1184+
i--;
1185+
return { value: array[i], done: false };
1186+
}
1187+
}
1188+
};
1189+
}
1190+
11761191
/**
11771192
* Stable sort of an array. Elements equal to each other maintain their relative position in the array.
11781193
*/

src/server/editorServices.ts

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,21 @@ namespace ts.server {
407407
}
408408
}
409409

410+
/*@internal*/
411+
export interface OpenFileArguments {
412+
fileName: string;
413+
content?: string;
414+
scriptKind?: protocol.ScriptKindName | ScriptKind;
415+
hasMixedContent?: boolean;
416+
projectRootPath?: string;
417+
}
418+
419+
/*@internal*/
420+
export interface ChangeFileArguments {
421+
fileName: string;
422+
changes: Iterator<TextChange>;
423+
}
424+
410425
export class ProjectService {
411426

412427
/*@internal*/
@@ -1128,11 +1143,22 @@ namespace ts.server {
11281143
return project;
11291144
}
11301145

1146+
private assignOrphanScriptInfosToInferredProject() {
1147+
// collect orphaned files and assign them to inferred project just like we treat open of a file
1148+
this.openFiles.forEach((projectRootPath, path) => {
1149+
const info = this.getScriptInfoForPath(path as Path)!;
1150+
// collect all orphaned script infos from open files
1151+
if (info.isOrphan()) {
1152+
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
1153+
}
1154+
});
1155+
}
1156+
11311157
/**
11321158
* Remove this file from the set of open, non-configured files.
11331159
* @param info The file that has been closed or newly configured
11341160
*/
1135-
private closeOpenFile(info: ScriptInfo): void {
1161+
private closeOpenFile(info: ScriptInfo, skipAssignOrphanScriptInfosToInferredProject?: true) {
11361162
// Closing file should trigger re-reading the file content from disk. This is
11371163
// because the user may chose to discard the buffer content before saving
11381164
// to the disk, and the server's version of the file can be out of sync.
@@ -1176,15 +1202,8 @@ namespace ts.server {
11761202

11771203
this.openFiles.delete(info.path);
11781204

1179-
if (ensureProjectsForOpenFiles) {
1180-
// collect orphaned files and assign them to inferred project just like we treat open of a file
1181-
this.openFiles.forEach((projectRootPath, path) => {
1182-
const info = this.getScriptInfoForPath(path as Path)!;
1183-
// collect all orphaned script infos from open files
1184-
if (info.isOrphan()) {
1185-
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
1186-
}
1187-
});
1205+
if (!skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles) {
1206+
this.assignOrphanScriptInfosToInferredProject();
11881207
}
11891208

11901209
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
@@ -1199,6 +1218,8 @@ namespace ts.server {
11991218
else {
12001219
this.handleDeletedFile(info);
12011220
}
1221+
1222+
return ensureProjectsForOpenFiles;
12021223
}
12031224

12041225
private deleteScriptInfo(info: ScriptInfo) {
@@ -2570,28 +2591,30 @@ namespace ts.server {
25702591
});
25712592
}
25722593

2573-
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult {
2574-
let configFileName: NormalizedPath | undefined;
2575-
let configFileErrors: ReadonlyArray<Diagnostic> | undefined;
2576-
2594+
private getOrCreateOpenScriptInfo(fileName: NormalizedPath, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, projectRootPath: NormalizedPath | undefined) {
25772595
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217
2578-
25792596
this.openFiles.set(info.path, projectRootPath);
2597+
return info;
2598+
}
2599+
2600+
private assignProjectToOpenedScriptInfo(info: ScriptInfo): OpenConfiguredProjectResult {
2601+
let configFileName: NormalizedPath | undefined;
2602+
let configFileErrors: ReadonlyArray<Diagnostic> | undefined;
25802603
let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
25812604
if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization
25822605
configFileName = this.getConfigFileNameForFile(info);
25832606
if (configFileName) {
25842607
project = this.findConfiguredProjectByProjectName(configFileName);
25852608
if (!project) {
2586-
project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${fileName} to open`);
2609+
project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`);
25872610
// Send the event only if the project got created as part of this open request and info is part of the project
25882611
if (info.isOrphan()) {
25892612
// Since the file isnt part of configured project, do not send config file info
25902613
configFileName = undefined;
25912614
}
25922615
else {
25932616
configFileErrors = project.getAllProjectErrors();
2594-
this.sendConfigFileDiagEvent(project, fileName);
2617+
this.sendConfigFileDiagEvent(project, info.fileName);
25952618
}
25962619
}
25972620
else {
@@ -2613,10 +2636,14 @@ namespace ts.server {
26132636
// At this point if file is part of any any configured or external project, then it would be present in the containing projects
26142637
// So if it still doesnt have any containing projects, it needs to be part of inferred project
26152638
if (info.isOrphan()) {
2616-
this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
2639+
Debug.assert(this.openFiles.has(info.path));
2640+
this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path));
26172641
}
26182642
Debug.assert(!info.isOrphan());
2643+
return { configFileName, configFileErrors };
2644+
}
26192645

2646+
private cleanupAfterOpeningFile() {
26202647
// This was postponed from closeOpenFile to after opening next file,
26212648
// so that we can reuse the project if we need to right away
26222649
this.removeOrphanConfiguredProjects();
@@ -2636,9 +2663,14 @@ namespace ts.server {
26362663
this.removeOrphanScriptInfos();
26372664

26382665
this.printProjects();
2666+
}
26392667

2668+
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult {
2669+
const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath);
2670+
const result = this.assignProjectToOpenedScriptInfo(info);
2671+
this.cleanupAfterOpeningFile();
26402672
this.telemetryOnOpenFile(info);
2641-
return { configFileName, configFileErrors };
2673+
return result;
26422674
}
26432675

26442676
private removeOrphanConfiguredProjects() {
@@ -2745,12 +2777,16 @@ namespace ts.server {
27452777
* Close file whose contents is managed by the client
27462778
* @param filename is absolute pathname
27472779
*/
2748-
closeClientFile(uncheckedFileName: string) {
2780+
closeClientFile(uncheckedFileName: string): void;
2781+
/*@internal*/
2782+
closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject: true): boolean;
2783+
closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject?: true) {
27492784
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
2750-
if (info) {
2751-
this.closeOpenFile(info);
2785+
const result = info ? this.closeOpenFile(info, skipAssignOrphanScriptInfosToInferredProject) : false;
2786+
if (!skipAssignOrphanScriptInfosToInferredProject) {
2787+
this.printProjects();
27522788
}
2753-
this.printProjects();
2789+
return result;
27542790
}
27552791

27562792
private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: ProjectFilesWithTSDiagnostics[]): void {
@@ -2770,36 +2806,68 @@ namespace ts.server {
27702806
}
27712807

27722808
/* @internal */
2773-
applyChangesInOpenFiles(openFiles: protocol.ExternalFile[] | undefined, changedFiles: protocol.ChangedOpenFile[] | undefined, closedFiles: string[] | undefined): void {
2809+
applyChangesInOpenFiles(openFiles: Iterator<OpenFileArguments> | undefined, changedFiles?: Iterator<ChangeFileArguments>, closedFiles?: string[]): void {
2810+
let openScriptInfos: ScriptInfo[] | undefined;
2811+
let assignOrphanScriptInfosToInferredProject = false;
27742812
if (openFiles) {
2775-
for (const file of openFiles) {
2813+
while (true) {
2814+
const { value: file, done } = openFiles.next();
2815+
if (done) break;
27762816
const scriptInfo = this.getScriptInfo(file.fileName);
27772817
Debug.assert(!scriptInfo || !scriptInfo.isScriptOpen(), "Script should not exist and not be open already");
2778-
const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName);
2779-
this.openClientFileWithNormalizedPath(normalizedPath, file.content, tryConvertScriptKindName(file.scriptKind!), file.hasMixedContent); // TODO: GH#18217
2818+
// Create script infos so we have the new content for all the open files before we do any updates to projects
2819+
const info = this.getOrCreateOpenScriptInfo(
2820+
scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName),
2821+
file.content,
2822+
tryConvertScriptKindName(file.scriptKind!),
2823+
file.hasMixedContent,
2824+
file.projectRootPath ? toNormalizedPath(file.projectRootPath) : undefined
2825+
);
2826+
(openScriptInfos || (openScriptInfos = [])).push(info);
27802827
}
27812828
}
27822829

27832830
if (changedFiles) {
2784-
for (const file of changedFiles) {
2831+
while (true) {
2832+
const { value: file, done } = changedFiles.next();
2833+
if (done) break;
27852834
const scriptInfo = this.getScriptInfo(file.fileName)!;
27862835
Debug.assert(!!scriptInfo);
2836+
// Make edits to script infos and marks containing project as dirty
27872837
this.applyChangesToFile(scriptInfo, file.changes);
27882838
}
27892839
}
27902840

27912841
if (closedFiles) {
27922842
for (const file of closedFiles) {
2793-
this.closeClientFile(file);
2843+
// Close files, but dont assign projects to orphan open script infos, that part comes later
2844+
assignOrphanScriptInfosToInferredProject = this.closeClientFile(file, /*skipAssignOrphanScriptInfosToInferredProject*/ true) || assignOrphanScriptInfosToInferredProject;
27942845
}
27952846
}
2847+
2848+
// All the script infos now exist, so ok to go update projects for open files
2849+
if (openScriptInfos) {
2850+
openScriptInfos.forEach(info => this.assignProjectToOpenedScriptInfo(info));
2851+
}
2852+
2853+
// While closing files there could be open files that needed assigning new inferred projects, do it now
2854+
if (assignOrphanScriptInfosToInferredProject) {
2855+
this.assignOrphanScriptInfosToInferredProject();
2856+
}
2857+
2858+
// Cleanup projects
2859+
this.cleanupAfterOpeningFile();
2860+
2861+
// Telemetry
2862+
forEach(openScriptInfos, info => this.telemetryOnOpenFile(info));
2863+
this.printProjects();
27962864
}
27972865

27982866
/* @internal */
2799-
applyChangesToFile(scriptInfo: ScriptInfo, changes: TextChange[]) {
2800-
// apply changes in reverse order
2801-
for (let i = changes.length - 1; i >= 0; i--) {
2802-
const change = changes[i];
2867+
applyChangesToFile(scriptInfo: ScriptInfo, changes: Iterator<TextChange>) {
2868+
while (true) {
2869+
const { value: change, done } = changes.next();
2870+
if (done) break;
28032871
scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText);
28042872
}
28052873
}

src/server/protocol.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ namespace ts.server.protocol {
9292
SynchronizeProjectList = "synchronizeProjectList",
9393
/* @internal */
9494
ApplyChangedToOpenFiles = "applyChangedToOpenFiles",
95+
UpdateOpen = "updateOpen",
9596
/* @internal */
9697
EncodedSemanticClassificationsFull = "encodedSemanticClassifications-full",
9798
/* @internal */
@@ -1543,6 +1544,32 @@ namespace ts.server.protocol {
15431544
closedFiles?: string[];
15441545
}
15451546

1547+
/**
1548+
* Request to synchronize list of open files with the client
1549+
*/
1550+
export interface UpdateOpenRequest extends Request {
1551+
command: CommandTypes.UpdateOpen;
1552+
arguments: UpdateOpenRequestArgs;
1553+
}
1554+
1555+
/**
1556+
* Arguments to UpdateOpenRequest
1557+
*/
1558+
export interface UpdateOpenRequestArgs {
1559+
/**
1560+
* List of newly open files
1561+
*/
1562+
openFiles?: OpenRequestArgs[];
1563+
/**
1564+
* List of open files files that were changes
1565+
*/
1566+
changedFiles?: FileCodeEdits[];
1567+
/**
1568+
* List of files that were closed
1569+
*/
1570+
closedFiles?: string[];
1571+
}
1572+
15461573
/**
15471574
* Request to set compiler options for inferred projects.
15481575
* External projects are opened / closed explicitly.

src/server/session.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,10 +1654,10 @@ namespace ts.server {
16541654
const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset);
16551655
if (start >= 0) {
16561656
this.changeSeq++;
1657-
this.projectService.applyChangesToFile(scriptInfo, [{
1657+
this.projectService.applyChangesToFile(scriptInfo, singleIterator({
16581658
span: { start, length: end - start },
16591659
newText: args.insertString! // TODO: GH#18217
1660-
}]);
1660+
}));
16611661
}
16621662
}
16631663

@@ -2096,9 +2096,39 @@ namespace ts.server {
20962096
});
20972097
return this.requiredResponse(converted);
20982098
},
2099+
[CommandNames.UpdateOpen]: (request: protocol.UpdateOpenRequest) => {
2100+
this.changeSeq++;
2101+
this.projectService.applyChangesInOpenFiles(
2102+
request.arguments.openFiles && mapIterator(arrayIterator(request.arguments.openFiles), file => ({
2103+
fileName: file.file,
2104+
content: file.fileContent,
2105+
scriptKind: file.scriptKindName,
2106+
projectRootPath: file.projectRootPath
2107+
})),
2108+
request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({
2109+
fileName: file.fileName,
2110+
changes: mapDefinedIterator(arrayReverseIterator(file.textChanges), change => {
2111+
const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file.fileName));
2112+
const start = scriptInfo.lineOffsetToPosition(change.start.line, change.start.offset);
2113+
const end = scriptInfo.lineOffsetToPosition(change.end.line, change.end.offset);
2114+
return start >= 0 ? { span: { start, length: end - start }, newText: change.newText } : undefined;
2115+
})
2116+
})),
2117+
request.arguments.closedFiles
2118+
);
2119+
return this.requiredResponse(/*response*/ true);
2120+
},
20992121
[CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => {
21002122
this.changeSeq++;
2101-
this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles!, request.arguments.closedFiles!); // TODO: GH#18217
2123+
this.projectService.applyChangesInOpenFiles(
2124+
request.arguments.openFiles && arrayIterator(request.arguments.openFiles),
2125+
request.arguments.changedFiles && mapIterator(arrayIterator(request.arguments.changedFiles), file => ({
2126+
fileName: file.fileName,
2127+
// apply changes in reverse order
2128+
changes: arrayReverseIterator(file.changes)
2129+
})),
2130+
request.arguments.closedFiles
2131+
);
21022132
// TODO: report errors
21032133
return this.requiredResponse(/*response*/ true);
21042134
},

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"unittests/tscWatch/resolutionCache.ts",
9797
"unittests/tscWatch/watchEnvironment.ts",
9898
"unittests/tscWatch/watchApi.ts",
99+
"unittests/tsserver/applyChangesToOpenFiles.ts",
99100
"unittests/tsserver/cachingFileSystemInformation.ts",
100101
"unittests/tsserver/cancellationToken.ts",
101102
"unittests/tsserver/compileOnSave.ts",

0 commit comments

Comments
 (0)