Skip to content

Commit e68161a

Browse files
authored
when language service is disabled - build program using only open files (#12809)
1 parent c71e6cc commit e68161a

File tree

6 files changed

+132
-108
lines changed

6 files changed

+132
-108
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,7 @@ namespace ts.projectSystem {
12381238
projectService.closeExternalProject(externalProjectName);
12391239
checkNumberOfProjects(projectService, { configuredProjects: 0 });
12401240
});
1241+
12411242
it("external project with included config file opened after configured project and then closed", () => {
12421243
const file1 = {
12431244
path: "/a/b/f1.ts",
@@ -1797,6 +1798,49 @@ namespace ts.projectSystem {
17971798
checkNumberOfProjects(projectService, { configuredProjects: 0 });
17981799
});
17991800

1801+
it("language service disabled state is updated in external projects", () => {
1802+
debugger
1803+
const f1 = {
1804+
path: "/a/app.js",
1805+
content: "var x = 1"
1806+
};
1807+
const f2 = {
1808+
path: "/a/largefile.js",
1809+
content: ""
1810+
};
1811+
const host = createServerHost([f1, f2]);
1812+
const originalGetFileSize = host.getFileSize;
1813+
host.getFileSize = (filePath: string) =>
1814+
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
1815+
1816+
const service = createProjectService(host);
1817+
const projectFileName = "/a/proj.csproj";
1818+
1819+
service.openExternalProject({
1820+
projectFileName,
1821+
rootFiles: toExternalFiles([f1.path, f2.path]),
1822+
options: {}
1823+
});
1824+
service.checkNumberOfProjects({ externalProjects: 1 });
1825+
assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1");
1826+
1827+
service.openExternalProject({
1828+
projectFileName,
1829+
rootFiles: toExternalFiles([f1.path]),
1830+
options: {}
1831+
});
1832+
service.checkNumberOfProjects({ externalProjects: 1 });
1833+
assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled");
1834+
1835+
service.openExternalProject({
1836+
projectFileName,
1837+
rootFiles: toExternalFiles([f1.path, f2.path]),
1838+
options: {}
1839+
});
1840+
service.checkNumberOfProjects({ externalProjects: 1 });
1841+
assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2");
1842+
});
1843+
18001844
it("language service disabled events are triggered", () => {
18011845
const f1 = {
18021846
path: "/a/app.js",

src/server/builder.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,32 @@ namespace ts.server {
7575
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
7676
onProjectUpdateGraph(): void;
7777
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
78+
clear(): void;
7879
}
7980

8081
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
8182

82-
private fileInfos = createFileMap<T>();
83+
/**
84+
* stores set of files from the project.
85+
* NOTE: this field is created on demand and should not be accessed directly.
86+
* Use 'getFileInfos' instead.
87+
*/
88+
private fileInfos_doNotAccessDirectly: FileMap<T>;
8389

8490
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
8591
}
8692

93+
private getFileInfos() {
94+
return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createFileMap<T>());
95+
}
96+
97+
public clear() {
98+
// drop the existing list - it will be re-created as necessary
99+
this.fileInfos_doNotAccessDirectly = undefined;
100+
}
101+
87102
protected getFileInfo(path: Path): T {
88-
return this.fileInfos.get(path);
103+
return this.getFileInfos().get(path);
89104
}
90105

91106
protected getOrCreateFileInfo(path: Path): T {
@@ -99,19 +114,19 @@ namespace ts.server {
99114
}
100115

101116
protected getFileInfoPaths(): Path[] {
102-
return this.fileInfos.getKeys();
117+
return this.getFileInfos().getKeys();
103118
}
104119

105120
protected setFileInfo(path: Path, info: T) {
106-
this.fileInfos.set(path, info);
121+
this.getFileInfos().set(path, info);
107122
}
108123

109124
protected removeFileInfo(path: Path) {
110-
this.fileInfos.remove(path);
125+
this.getFileInfos().remove(path);
111126
}
112127

113128
protected forEachFileInfo(action: (fileInfo: T) => any) {
114-
this.fileInfos.forEachValue((_path, value) => action(value));
129+
this.getFileInfos().forEachValue((_path, value) => action(value));
115130
}
116131

117132
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
@@ -231,6 +246,11 @@ namespace ts.server {
231246

232247
private projectVersionForDependencyGraph: string;
233248

249+
public clear() {
250+
this.projectVersionForDependencyGraph = undefined;
251+
super.clear();
252+
}
253+
234254
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] {
235255
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
236256
return [];

src/server/editorServices.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ namespace ts.server {
126126
}
127127

128128
export interface OpenConfiguredProjectResult {
129-
configFileName?: string;
129+
configFileName?: NormalizedPath;
130130
configFileErrors?: Diagnostic[];
131131
}
132132

@@ -659,6 +659,13 @@ namespace ts.server {
659659
// open file in inferred project
660660
(projectsToRemove || (projectsToRemove = [])).push(p);
661661
}
662+
663+
if (!p.languageServiceEnabled) {
664+
// if project language service is disabled then we create a program only for open files.
665+
// this means that project should be marked as dirty to force rebuilding of the program
666+
// on the next request
667+
p.markAsDirty();
668+
}
662669
}
663670
if (projectsToRemove) {
664671
for (const project of projectsToRemove) {
@@ -1052,9 +1059,7 @@ namespace ts.server {
10521059
project.stopWatchingDirectory();
10531060
}
10541061
else {
1055-
if (!project.languageServiceEnabled) {
1056-
project.enableLanguageService();
1057-
}
1062+
project.enableLanguageService();
10581063
this.watchConfigDirectoryForProject(project, projectOptions);
10591064
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors);
10601065
}
@@ -1226,9 +1231,22 @@ namespace ts.server {
12261231
}
12271232

12281233
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult {
1229-
const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName)
1230-
? {}
1231-
: this.openOrUpdateConfiguredProjectForFile(fileName);
1234+
let configFileName: NormalizedPath;
1235+
let configFileErrors: Diagnostic[];
1236+
1237+
let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName);
1238+
if (!project) {
1239+
({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName));
1240+
if (configFileName) {
1241+
project = this.findConfiguredProjectByProjectName(configFileName);
1242+
}
1243+
}
1244+
if (project && !project.languageServiceEnabled) {
1245+
// if project language service is disabled then we create a program only for open files.
1246+
// this means that project should be marked as dirty to force rebuilding of the program
1247+
// on the next request
1248+
project.markAsDirty();
1249+
}
12321250

12331251
// at this point if file is the part of some configured/external project then this project should be created
12341252
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
@@ -1392,8 +1410,15 @@ namespace ts.server {
13921410
let exisingConfigFiles: string[];
13931411
if (externalProject) {
13941412
if (!tsConfigFiles) {
1413+
const compilerOptions = convertCompilerOptions(proj.options);
1414+
if (this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, proj.rootFiles, externalFilePropertyReader)) {
1415+
externalProject.disableLanguageService();
1416+
}
1417+
else {
1418+
externalProject.enableLanguageService();
1419+
}
13951420
// external project already exists and not config files were added - update the project and return;
1396-
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, convertCompilerOptions(proj.options), proj.typeAcquisition, proj.options.compileOnSave, /*configFileErrors*/ undefined);
1421+
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, /*configFileErrors*/ undefined);
13971422
return;
13981423
}
13991424
// some config files were added to external project (that previously were not there)

src/server/project.ts

Lines changed: 21 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -90,87 +90,6 @@ namespace ts.server {
9090
}
9191
}
9292

93-
const emptyResult: any[] = [];
94-
const getEmptyResult = () => emptyResult;
95-
const getUndefined = () => <any>undefined;
96-
const emptyEncodedSemanticClassifications = { spans: emptyResult, endOfLineState: EndOfLineState.None };
97-
98-
export function createNoSemanticFeaturesWrapper(realLanguageService: LanguageService): LanguageService {
99-
return {
100-
cleanupSemanticCache: noop,
101-
getSyntacticDiagnostics: (fileName) =>
102-
fileName ? realLanguageService.getSyntacticDiagnostics(fileName) : emptyResult,
103-
getSemanticDiagnostics: getEmptyResult,
104-
getCompilerOptionsDiagnostics: () =>
105-
realLanguageService.getCompilerOptionsDiagnostics(),
106-
getSyntacticClassifications: (fileName, span) =>
107-
realLanguageService.getSyntacticClassifications(fileName, span),
108-
getEncodedSyntacticClassifications: (fileName, span) =>
109-
realLanguageService.getEncodedSyntacticClassifications(fileName, span),
110-
getSemanticClassifications: getEmptyResult,
111-
getEncodedSemanticClassifications: () =>
112-
emptyEncodedSemanticClassifications,
113-
getCompletionsAtPosition: getUndefined,
114-
findReferences: getEmptyResult,
115-
getCompletionEntryDetails: getUndefined,
116-
getQuickInfoAtPosition: getUndefined,
117-
findRenameLocations: getEmptyResult,
118-
getNameOrDottedNameSpan: (fileName, startPos, endPos) =>
119-
realLanguageService.getNameOrDottedNameSpan(fileName, startPos, endPos),
120-
getBreakpointStatementAtPosition: (fileName, position) =>
121-
realLanguageService.getBreakpointStatementAtPosition(fileName, position),
122-
getBraceMatchingAtPosition: (fileName, position) =>
123-
realLanguageService.getBraceMatchingAtPosition(fileName, position),
124-
getSignatureHelpItems: getUndefined,
125-
getDefinitionAtPosition: getEmptyResult,
126-
getRenameInfo: () => ({
127-
canRename: false,
128-
localizedErrorMessage: getLocaleSpecificMessage(Diagnostics.Language_service_is_disabled),
129-
displayName: undefined,
130-
fullDisplayName: undefined,
131-
kind: undefined,
132-
kindModifiers: undefined,
133-
triggerSpan: undefined
134-
}),
135-
getTypeDefinitionAtPosition: getUndefined,
136-
getReferencesAtPosition: getEmptyResult,
137-
getDocumentHighlights: getEmptyResult,
138-
getOccurrencesAtPosition: getEmptyResult,
139-
getNavigateToItems: getEmptyResult,
140-
getNavigationBarItems: fileName =>
141-
realLanguageService.getNavigationBarItems(fileName),
142-
getNavigationTree: fileName =>
143-
realLanguageService.getNavigationTree(fileName),
144-
getOutliningSpans: fileName =>
145-
realLanguageService.getOutliningSpans(fileName),
146-
getTodoComments: getEmptyResult,
147-
getIndentationAtPosition: (fileName, position, options) =>
148-
realLanguageService.getIndentationAtPosition(fileName, position, options),
149-
getFormattingEditsForRange: (fileName, start, end, options) =>
150-
realLanguageService.getFormattingEditsForRange(fileName, start, end, options),
151-
getFormattingEditsForDocument: (fileName, options) =>
152-
realLanguageService.getFormattingEditsForDocument(fileName, options),
153-
getFormattingEditsAfterKeystroke: (fileName, position, key, options) =>
154-
realLanguageService.getFormattingEditsAfterKeystroke(fileName, position, key, options),
155-
getDocCommentTemplateAtPosition: (fileName, position) =>
156-
realLanguageService.getDocCommentTemplateAtPosition(fileName, position),
157-
isValidBraceCompletionAtPosition: (fileName, position, openingBrace) =>
158-
realLanguageService.isValidBraceCompletionAtPosition(fileName, position, openingBrace),
159-
getEmitOutput: getUndefined,
160-
getProgram: () =>
161-
realLanguageService.getProgram(),
162-
getNonBoundSourceFile: fileName =>
163-
realLanguageService.getNonBoundSourceFile(fileName),
164-
dispose: () =>
165-
realLanguageService.dispose(),
166-
getCompletionEntrySymbol: getUndefined,
167-
getImplementationAtPosition: getEmptyResult,
168-
getSourceFile: fileName =>
169-
realLanguageService.getSourceFile(fileName),
170-
getCodeFixesAtPosition: getEmptyResult
171-
};
172-
}
173-
17493
export abstract class Project {
17594
private rootFiles: ScriptInfo[] = [];
17695
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
@@ -181,8 +100,6 @@ namespace ts.server {
181100
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
182101

183102
private readonly languageService: LanguageService;
184-
// wrapper over the real language service that will suppress all semantic operations
185-
private readonly noSemanticFeaturesLanguageService: LanguageService;
186103

187104
public languageServiceEnabled = true;
188105

@@ -258,7 +175,6 @@ namespace ts.server {
258175
this.lsHost.setCompilationSettings(this.compilerOptions);
259176

260177
this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry);
261-
this.noSemanticFeaturesLanguageService = createNoSemanticFeaturesWrapper(this.languageService);
262178

263179
if (!languageServiceEnabled) {
264180
this.disableLanguageService();
@@ -282,9 +198,7 @@ namespace ts.server {
282198
if (ensureSynchronized) {
283199
this.updateGraph();
284200
}
285-
return this.languageServiceEnabled
286-
? this.languageService
287-
: this.noSemanticFeaturesLanguageService;
201+
return this.languageService;
288202
}
289203

290204
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
@@ -373,7 +287,10 @@ namespace ts.server {
373287
const result: string[] = [];
374288
if (this.rootFiles) {
375289
for (const f of this.rootFiles) {
376-
result.push(f.fileName);
290+
if (this.languageServiceEnabled || f.isScriptOpen()) {
291+
// if language service is disabled - process only files that are open
292+
result.push(f.fileName);
293+
}
377294
}
378295
if (this.typingFiles) {
379296
for (const f of this.typingFiles) {
@@ -389,6 +306,10 @@ namespace ts.server {
389306
}
390307

391308
getScriptInfos() {
309+
if (!this.languageServiceEnabled) {
310+
// if language service is not enabled - return just root files
311+
return this.rootFiles;
312+
}
392313
return map(this.program.getSourceFiles(), sourceFile => {
393314
const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path);
394315
if (!scriptInfo) {
@@ -529,10 +450,6 @@ namespace ts.server {
529450
* @returns: true if set of files in the project stays the same and false - otherwise.
530451
*/
531452
updateGraph(): boolean {
532-
if (!this.languageServiceEnabled) {
533-
return true;
534-
}
535-
536453
this.lsHost.startRecordingFilesWithChangedResolutions();
537454

538455
let hasChanges = this.updateGraphWorker();
@@ -564,6 +481,16 @@ namespace ts.server {
564481
if (this.setTypings(cachedTypings)) {
565482
hasChanges = this.updateGraphWorker() || hasChanges;
566483
}
484+
485+
// update builder only if language service is enabled
486+
// otherwise tell it to drop its internal state
487+
if (this.languageServiceEnabled) {
488+
this.builder.onProjectUpdateGraph();
489+
}
490+
else {
491+
this.builder.clear();
492+
}
493+
567494
if (hasChanges) {
568495
this.projectStructureVersion++;
569496
}
@@ -602,7 +529,6 @@ namespace ts.server {
602529
}
603530
}
604531
}
605-
this.builder.onProjectUpdateGraph();
606532
return hasChanges;
607533
}
608534

@@ -673,7 +599,8 @@ namespace ts.server {
673599
projectName: this.getProjectName(),
674600
version: this.projectStructureVersion,
675601
isInferred: this.projectKind === ProjectKind.Inferred,
676-
options: this.getCompilerOptions()
602+
options: this.getCompilerOptions(),
603+
languageServiceDisabled: !this.languageServiceEnabled
677604
};
678605
const updatedFileNames = this.updatedFileNames;
679606
this.updatedFileNames = undefined;

0 commit comments

Comments
 (0)