Skip to content

Commit cf4536b

Browse files
committed
feat: lazy initialization on watched file changes
1 parent 525c569 commit cf4536b

File tree

3 files changed

+127
-81
lines changed

3 files changed

+127
-81
lines changed

packages/graphql-language-service-server/src/MessageProcessor.ts

Lines changed: 63 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import {
6363
ConfigEmptyError,
6464
ConfigInvalidError,
6565
ConfigNotFoundError,
66-
GraphQLExtensionDeclaration,
6766
LoaderNoResultError,
6867
ProjectNotFoundError,
6968
} from 'graphql-config';
@@ -88,24 +87,22 @@ function toPosition(position: VscodePosition): IPosition {
8887
}
8988

9089
export class MessageProcessor {
91-
_connection: Connection;
92-
_graphQLCache!: GraphQLCache;
93-
_graphQLConfig: GraphQLConfig | undefined;
94-
_languageService!: GraphQLLanguageService;
95-
_textDocumentCache = new Map<string, CachedDocumentType>();
96-
_isInitialized = false;
97-
_isGraphQLConfigMissing: boolean | null = null;
98-
_willShutdown = false;
99-
_logger: Logger | NoopLogger;
100-
_extensions?: GraphQLExtensionDeclaration[];
101-
_parser: (text: string, uri: string) => CachedContent[];
102-
_tmpDir: string;
103-
_tmpUriBase: string;
104-
_tmpDirBase: string;
105-
_loadConfigOptions: LoadConfigOptions;
106-
_schemaCacheInit = false;
107-
_rootPath: string = process.cwd();
108-
_settings: any;
90+
private _connection: Connection;
91+
private _graphQLCache!: GraphQLCache;
92+
private _languageService!: GraphQLLanguageService;
93+
private _textDocumentCache = new Map<string, CachedDocumentType>();
94+
private _isInitialized = false;
95+
private _isGraphQLConfigMissing: boolean | null = null;
96+
private _willShutdown = false;
97+
private _logger: Logger | NoopLogger;
98+
private _parser: (text: string, uri: string) => CachedContent[];
99+
private _tmpDir: string;
100+
private _tmpUriBase: string;
101+
private _tmpDirBase: string;
102+
private _loadConfigOptions: LoadConfigOptions;
103+
private _schemaCacheInit = false;
104+
private _rootPath: string = process.cwd();
105+
private _settings: any;
109106

110107
constructor({
111108
logger,
@@ -128,7 +125,6 @@ export class MessageProcessor {
128125
}) {
129126
this._connection = connection;
130127
this._logger = logger;
131-
this._graphQLConfig = config;
132128
this._parser = (text, uri) => {
133129
const p = parser ?? parseDocument;
134130
return p(text, uri, fileExtensions, graphqlFileExtensions, this._logger);
@@ -138,12 +134,6 @@ export class MessageProcessor {
138134
this._tmpUriBase = URI.file(this._tmpDirBase).toString();
139135
// use legacy mode by default for backwards compatibility
140136
this._loadConfigOptions = { legacy: true, ...loadConfigOptions };
141-
if (
142-
loadConfigOptions.extensions &&
143-
loadConfigOptions.extensions?.length > 0
144-
) {
145-
this._extensions = loadConfigOptions.extensions;
146-
}
147137

148138
if (!existsSync(this._tmpDirBase)) {
149139
void mkdirSync(this._tmpDirBase);
@@ -156,7 +146,7 @@ export class MessageProcessor {
156146
this._connection = connection;
157147
}
158148

159-
async handleInitializeRequest(
149+
public async handleInitializeRequest(
160150
params: InitializeParams,
161151
_token?: CancellationToken,
162152
configDir?: string,
@@ -244,7 +234,6 @@ export class MessageProcessor {
244234
);
245235
const config = this._graphQLCache.getGraphQLConfig();
246236
if (config) {
247-
this._graphQLConfig = config;
248237
await this._cacheAllProjectFiles(config);
249238
this._isInitialized = true;
250239
this._isGraphQLConfigMissing = false;
@@ -312,29 +301,44 @@ export class MessageProcessor {
312301
}
313302
return false;
314303
}
315-
316-
async handleDidOpenOrSaveNotification(
317-
params: DidSaveTextDocumentParams | DidOpenTextDocumentParams,
318-
): Promise<PublishDiagnosticsParams> {
319-
/**
320-
* Initialize the LSP server when the first file is opened or saved,
321-
* so that we can access the user settings for config rootDir, etc
322-
*/
323-
const isGraphQLConfigFile = await this._isGraphQLConfigFile(
324-
params.textDocument.uri,
325-
);
304+
private async _loadConfigOrSkip(uri: string) {
326305
try {
306+
const isGraphQLConfigFile = await this._isGraphQLConfigFile(uri);
307+
327308
if (!this._isInitialized) {
328-
// don't try to initialize again if we've already tried
329-
// and the graphql config file or package.json entry isn't even there
330309
if (this._isGraphQLConfigMissing === true && !isGraphQLConfigFile) {
331-
return { uri: params.textDocument.uri, diagnostics: [] };
310+
return true;
332311
}
333-
// then initial call to update graphql config
312+
// don't try to initialize again if we've already tried
313+
// and the graphql config file or package.json entry isn't even there
314+
await this._updateGraphQLConfig();
315+
return isGraphQLConfigFile;
316+
}
317+
// if it has initialized, but this is another config file change, then let's handle it
318+
if (isGraphQLConfigFile) {
334319
await this._updateGraphQLConfig();
335320
}
321+
return isGraphQLConfigFile;
336322
} catch (err) {
337323
this._logger.error(String(err));
324+
// return true if it's a graphql config file so we don't treat
325+
// this as a non-config file if it is one
326+
return true;
327+
}
328+
}
329+
330+
public async handleDidOpenOrSaveNotification(
331+
params: DidSaveTextDocumentParams | DidOpenTextDocumentParams,
332+
): Promise<PublishDiagnosticsParams> {
333+
/**
334+
* Initialize the LSP server when the first file is opened or saved,
335+
* so that we can access the user settings for config rootDir, etc
336+
*/
337+
const shouldSkip = await this._loadConfigOrSkip(params.textDocument.uri);
338+
// if we're loading config or the config is missing or there's an error
339+
// don't do anything else
340+
if (shouldSkip) {
341+
return { uri: params.textDocument.uri, diagnostics: [] };
338342
}
339343

340344
// Here, we set the workspace settings in memory,
@@ -361,20 +365,13 @@ export class MessageProcessor {
361365
contents = this._parser(text, uri);
362366

363367
await this._invalidateCache(textDocument, uri, contents);
364-
} else if (isGraphQLConfigFile) {
365-
this._logger.info('updating graphql config');
366-
await this._updateGraphQLConfig();
367-
return { uri, diagnostics: [] };
368368
}
369369
if (!this._graphQLCache) {
370370
return { uri, diagnostics };
371371
}
372372
try {
373373
const project = this._graphQLCache.getProjectForFile(uri);
374-
if (
375-
this._isInitialized &&
376-
project?.extensions?.languageService?.enableValidation !== false
377-
) {
374+
if (project?.extensions?.languageService?.enableValidation !== false) {
378375
await Promise.all(
379376
contents.map(async ({ query, range }) => {
380377
const results = await this._languageService.getDiagnostics(
@@ -406,7 +403,7 @@ export class MessageProcessor {
406403
return { uri, diagnostics };
407404
}
408405

409-
async handleDidChangeNotification(
406+
public async handleDidChangeNotification(
410407
params: DidChangeTextDocumentParams,
411408
): Promise<PublishDiagnosticsParams | null> {
412409
if (
@@ -497,7 +494,7 @@ export class MessageProcessor {
497494
return {};
498495
}
499496

500-
handleDidCloseNotification(params: DidCloseTextDocumentParams): void {
497+
public handleDidCloseNotification(params: DidCloseTextDocumentParams): void {
501498
if (!this._isInitialized) {
502499
return;
503500
}
@@ -525,11 +522,11 @@ export class MessageProcessor {
525522
);
526523
}
527524

528-
handleShutdownRequest(): void {
525+
public handleShutdownRequest(): void {
529526
this._willShutdown = true;
530527
}
531528

532-
handleExitNotification(): void {
529+
public handleExitNotification(): void {
533530
process.exit(this._willShutdown ? 0 : 1);
534531
}
535532

@@ -541,7 +538,7 @@ export class MessageProcessor {
541538
}
542539
}
543540

544-
async handleCompletionRequest(
541+
public async handleCompletionRequest(
545542
params: CompletionParams,
546543
): Promise<CompletionList> {
547544
if (!this._isInitialized) {
@@ -599,7 +596,9 @@ export class MessageProcessor {
599596
return { items: result, isIncomplete: false };
600597
}
601598

602-
async handleHoverRequest(params: TextDocumentPositionParams): Promise<Hover> {
599+
public async handleHoverRequest(
600+
params: TextDocumentPositionParams,
601+
): Promise<Hover> {
603602
if (!this._isInitialized) {
604603
return { contents: [] };
605604
}
@@ -642,7 +641,7 @@ export class MessageProcessor {
642641
};
643642
}
644643

645-
async handleWatchedFilesChangedNotification(
644+
public async handleWatchedFilesChangedNotification(
646645
params: DidChangeWatchedFilesParams,
647646
): Promise<Array<PublishDiagnosticsParams | undefined> | null> {
648647
if (
@@ -655,13 +654,9 @@ export class MessageProcessor {
655654

656655
return Promise.all(
657656
params.changes.map(async (change: FileEvent) => {
658-
if (
659-
this._isGraphQLConfigMissing ||
660-
!this._isInitialized ||
661-
!this._graphQLCache
662-
) {
663-
this._logger.warn('No cache available for handleWatchedFilesChanged');
664-
return;
657+
const shouldSkip = await this._loadConfigOrSkip(change.uri);
658+
if (shouldSkip) {
659+
return { uri: change.uri, diagnostics: [] };
665660
}
666661
if (
667662
change.type === FileChangeTypeKind.Created ||
@@ -731,7 +726,7 @@ export class MessageProcessor {
731726
);
732727
}
733728

734-
async handleDefinitionRequest(
729+
public async handleDefinitionRequest(
735730
params: TextDocumentPositionParams,
736731
_token?: CancellationToken,
737732
): Promise<Array<Location>> {
@@ -836,7 +831,7 @@ export class MessageProcessor {
836831
return formatted;
837832
}
838833

839-
async handleDocumentSymbolRequest(
834+
public async handleDocumentSymbolRequest(
840835
params: DocumentSymbolParams,
841836
): Promise<Array<SymbolInformation>> {
842837
if (!this._isInitialized) {
@@ -896,7 +891,7 @@ export class MessageProcessor {
896891
// );
897892
// }
898893

899-
async handleWorkspaceSymbolRequest(
894+
public async handleWorkspaceSymbolRequest(
900895
params: WorkspaceSymbolParams,
901896
): Promise<Array<SymbolInformation>> {
902897
if (!this._isInitialized) {

packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,31 @@ describe('MessageProcessor with no config', () => {
4242
files: [...defaultFiles, ['graphql.config.json', '']],
4343
});
4444
await project.init();
45-
expect(project.lsp._isInitialized).toEqual(false);
46-
expect(project.lsp._isGraphQLConfigMissing).toEqual(true);
45+
4746
expect(project.lsp._logger.info).toHaveBeenCalledTimes(1);
4847
expect(project.lsp._logger.error).toHaveBeenCalledTimes(1);
4948
expect(project.lsp._logger.error).toHaveBeenCalledWith(
5049
expect.stringMatching(
5150
/GraphQL Config file is not available in the provided config directory/,
5251
),
5352
);
53+
expect(project.lsp._isInitialized).toEqual(false);
54+
expect(project.lsp._isGraphQLConfigMissing).toEqual(true);
5455
});
5556
it('fails to initialize with no config file present', async () => {
5657
const project = new MockProject({
5758
files: [...defaultFiles],
5859
});
5960
await project.init();
60-
expect(project.lsp._isInitialized).toEqual(false);
61-
expect(project.lsp._isGraphQLConfigMissing).toEqual(true);
61+
6262
expect(project.lsp._logger.error).toHaveBeenCalledTimes(1);
6363
expect(project.lsp._logger.error).toHaveBeenCalledWith(
6464
expect.stringMatching(
6565
/GraphQL Config file is not available in the provided config directory/,
6666
),
6767
);
68+
expect(project.lsp._isInitialized).toEqual(false);
69+
expect(project.lsp._isGraphQLConfigMissing).toEqual(true);
6870
});
6971
it('initializes when presented with a valid config later', async () => {
7072
const project = new MockProject({
@@ -246,9 +248,8 @@ describe('project with simple config and graphql files', () => {
246248
fooLaterTypePosition,
247249
);
248250
expect(project.lsp._logger.error).not.toHaveBeenCalled();
249-
250-
// TODO: the position should change when a watched file changes???
251251
});
252+
252253
it('caches files and schema with a URL config', async () => {
253254
const project = new MockProject({
254255
files: [
@@ -262,10 +263,10 @@ describe('project with simple config and graphql files', () => {
262263
});
263264

264265
const initParams = await project.init('query.graphql');
265-
expect(initParams.diagnostics).toEqual([]);
266-
267266
expect(project.lsp._logger.error).not.toHaveBeenCalled();
268267

268+
expect(initParams.diagnostics).toEqual([]);
269+
269270
const changeParams = await project.lsp.handleDidChangeNotification({
270271
textDocument: { uri: project.uri('query.graphql'), version: 1 },
271272
contentChanges: [{ text: 'query { test { isTest, ...T or } }' }],
@@ -373,6 +374,7 @@ describe('project with simple config and graphql files', () => {
373374
});
374375
expect(project.lsp._logger.error).not.toHaveBeenCalled();
375376
});
377+
376378
it('caches multiple projects with files and schema with a URL config and a local schema', async () => {
377379
const project = new MockProject({
378380
files: [

0 commit comments

Comments
 (0)