Skip to content

Commit aefaa44

Browse files
committed
feat(apidom-ls): document cache
1 parent a93d428 commit aefaa44

15 files changed

+340
-191
lines changed

packages/apidom-ls/src/apidom-language-service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from 'vscode-languageserver-types';
99
import { TextDocument } from 'vscode-languageserver-textdocument';
1010
import { SemanticTokensLegend } from 'vscode-languageserver-protocol';
11+
import { ParseResultElement } from '@swagger-api/apidom-core';
1112

1213
import {
1314
ColorsContext,
@@ -22,6 +23,8 @@ import { DefaultSemanticTokensService } from './services/semantic-tokens/semanti
2223
import { DefaultHoverService } from './services/hover/hover-service';
2324
import { DefaultDerefService } from './services/deref/deref-service';
2425
import { DefaultDefinitionService } from './services/definition/definition-service';
26+
import { getDocumentCache } from './document-cache';
27+
import { parse } from './parser-factory';
2528

2629
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2730
export default function getLanguageService(context: LanguageServiceContext): LanguageService {
@@ -43,12 +46,17 @@ export default function getLanguageService(context: LanguageServiceContext): Lan
4346
definitionService.configure(languageSettings);
4447
}
4548

49+
const documentCache = getDocumentCache<ParseResultElement>(10, 60, (document) =>
50+
parse(document, context?.metadata?.metadataMaps),
51+
);
52+
4653
// TODO solve init and config
4754
if (context.metadata) {
4855
const languageSettings: LanguageSettings = {
4956
metadata: context.metadata,
5057
validate: true,
5158
validatorProviders: context.validatorProviders,
59+
documentCache,
5260
};
5361
configureServices(languageSettings);
5462
}
@@ -93,5 +101,8 @@ export default function getLanguageService(context: LanguageServiceContext): Lan
93101
getColorPresentations(document: TextDocument, color: Color, range: Range): ColorPresentation[] {
94102
return [];
95103
},
104+
terminate(): void {
105+
documentCache.dispose();
106+
},
96107
};
97108
}

packages/apidom-ls/src/apidom-language-types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import {
2424
DefinitionParams,
2525
ReferenceParams,
2626
} from 'vscode-languageserver-protocol';
27-
import { Element } from 'minim';
27+
import { Element, ParseResultElement } from '@swagger-api/apidom-core';
2828

2929
import { Metadata } from './utils/utils';
30+
import { DocumentCache } from './document-cache';
3031

3132
// eslint-disable-next-line @typescript-eslint/naming-convention
3233
export enum SUPPORTED_LANGUAGES {
@@ -68,6 +69,7 @@ export interface LanguageSettings {
6869
allowComments?: boolean;
6970
validatorProviders?: ValidationProvider[];
7071
metadata?: Metadata;
72+
documentCache?: DocumentCache<ParseResultElement>;
7173
}
7274

7375
// export type SeverityLevel = 'error' | 'warning' | 'ignore';
@@ -147,4 +149,5 @@ export interface LanguageService {
147149
getColorPresentations(document: TextDocument, color: Color, range: Range): ColorPresentation[];
148150

149151
format(document: TextDocument, range: Range, options: FormattingOptions): TextEdit[];
152+
terminate(): void;
150153
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { TextDocument } from 'vscode-languageserver-textdocument';
2+
3+
/*
4+
Adapted from https://github.com/microsoft/vscode/blob/main/extensions/json-language-features/server/src/languageModelCache.ts
5+
*/
6+
export interface DocumentCache<T> {
7+
get(document: TextDocument): Promise<T | undefined>;
8+
onDocumentRemoved(document: TextDocument): void;
9+
dispose(): void;
10+
}
11+
12+
export function getDocumentCache<T>(
13+
maxEntries: number,
14+
cleanupIntervalTimeInSec: number,
15+
parse: (document: TextDocument) => Promise<T>,
16+
): DocumentCache<T> {
17+
let documents: {
18+
[uri: string]: {
19+
version: number;
20+
languageId: string;
21+
cTime: number;
22+
parsedDocument: T;
23+
};
24+
} = {};
25+
let nModels = 0;
26+
27+
let cleanupInterval: ReturnType<typeof setInterval> | undefined;
28+
if (cleanupIntervalTimeInSec > 0) {
29+
cleanupInterval = setInterval(() => {
30+
const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
31+
const uris = Object.keys(documents);
32+
for (const uri of uris) {
33+
const documentInfo = documents[uri];
34+
if (documentInfo.cTime < cutoffTime) {
35+
delete documents[uri];
36+
// eslint-disable-next-line no-plusplus
37+
nModels--;
38+
}
39+
}
40+
}, cleanupIntervalTimeInSec * 1000);
41+
}
42+
43+
return {
44+
async get(document: TextDocument): Promise<T> {
45+
const { version } = document;
46+
const { languageId } = document;
47+
const documentInfo = documents[document.uri];
48+
if (
49+
documentInfo &&
50+
documentInfo.version === version &&
51+
documentInfo.languageId === languageId
52+
) {
53+
documentInfo.cTime = Date.now();
54+
return documentInfo.parsedDocument;
55+
}
56+
const parsedDocument = await parse(document);
57+
documents[document.uri] = {
58+
parsedDocument,
59+
version,
60+
languageId,
61+
cTime: Date.now(),
62+
};
63+
if (!documentInfo) {
64+
// eslint-disable-next-line no-plusplus
65+
nModels++;
66+
}
67+
68+
if (nModels === maxEntries) {
69+
let oldestTime = Number.MAX_VALUE;
70+
let oldestUri = null;
71+
// eslint-disable-next-line guard-for-in
72+
for (const uri in documents) {
73+
const documentInfoInstance = documents[uri];
74+
if (documentInfoInstance.cTime < oldestTime) {
75+
oldestUri = uri;
76+
oldestTime = documentInfoInstance.cTime;
77+
}
78+
}
79+
if (oldestUri) {
80+
delete documents[oldestUri];
81+
// eslint-disable-next-line no-plusplus
82+
nModels--;
83+
}
84+
}
85+
return parsedDocument;
86+
},
87+
onDocumentRemoved(document: TextDocument) {
88+
const { uri } = document;
89+
if (documents[uri]) {
90+
delete documents[uri];
91+
// eslint-disable-next-line no-plusplus
92+
nModels--;
93+
}
94+
},
95+
dispose() {
96+
if (typeof cleanupInterval !== 'undefined') {
97+
clearInterval(cleanupInterval);
98+
cleanupInterval = undefined;
99+
documents = {};
100+
nModels = 0;
101+
}
102+
},
103+
};
104+
}

packages/apidom-ls/src/parser-factory.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import * as openapi3_1Adapter_Yaml from '@swagger-api/apidom-parser-adapter-open
99
// @ts-ignore
1010
import * as asyncapi2Adapter_Yaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2';
1111
import { TextDocument } from 'vscode-languageserver-textdocument';
12+
import { ParseResultElement } from '@swagger-api/apidom-core';
13+
14+
import { setMetadataMap, MetadataMaps } from './utils/utils';
1215

1316
export interface ParserOptions {
1417
sourceMap?: boolean;
@@ -63,3 +66,20 @@ export function getParser(document: TextDocument): ApiDOMParser {
6366
}
6467
return ApiDOMParser().use(openapi3_1Adapter);
6568
}
69+
70+
export async function parse(
71+
textDocument: TextDocument,
72+
metadataMaps: MetadataMaps | undefined,
73+
): Promise<ParseResultElement> {
74+
const parser = getParser(textDocument);
75+
const text: string = textDocument.getText();
76+
const result = await parser.parse(text, { sourceMap: true });
77+
const { api } = result;
78+
if (api === undefined) return result;
79+
const docNs: string = isAsyncDoc(text) ? 'asyncapi' : 'openapi';
80+
// TODO (francesco@tumanischvili@smartbear.com) use the type related metadata at root level defining the tokenTypes and modifiers
81+
setMetadataMap(api, docNs, metadataMaps); // TODO (francesco@tumanischvili@smartbear.com) move to parser/adapter, extending the one standard
82+
api.freeze(); // !! freeze and add parent !!
83+
84+
return result;
85+
}

packages/apidom-ls/src/services/completion/completion-service.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import {
22
CompletionItem,
33
CompletionItemKind,
44
CompletionList,
5-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6-
InsertTextFormat,
75
Position,
86
Range,
97
TextEdit,
@@ -24,9 +22,8 @@ import {
2422
} from '@swagger-api/apidom-core';
2523

2624
import { LanguageSettings, CompletionContext } from '../../apidom-language-types';
27-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
28-
import { setMetadataMap, getSourceMap, isMember, isObject } from '../../utils/utils';
29-
import { getParser, isJsonDoc, isAsyncDoc } from '../../parser-factory';
25+
import { getSourceMap, isMember, isObject } from '../../utils/utils';
26+
import { isJsonDoc } from '../../parser-factory';
3027

3128
export interface CompletionsCollector {
3229
add(suggestion: unknown): void;
@@ -171,7 +168,6 @@ export class DefaultCompletionService implements CompletionService {
171168
: completionParamsOrPosition;
172169

173170
// get right parser
174-
const parser = getParser(textDocument);
175171
const text: string = textDocument.getText();
176172

177173
const schema = false;
@@ -194,19 +190,12 @@ export class DefaultCompletionService implements CompletionService {
194190
}
195191

196192
// parse
197-
const { api } = await parser.parse(text, { sourceMap: true });
198-
193+
const result = await this.settings!.documentCache?.get(textDocument);
194+
if (!result) return CompletionList.create();
195+
const { api } = result;
199196
// if we cannot parse nothing to do
200197
if (api === undefined) return CompletionList.create();
201198

202-
// use the type related metadata at root level
203-
setMetadataMap(
204-
api,
205-
isAsyncDoc(text) ? 'asyncapi' : 'openapi',
206-
this.settings?.metadata?.metadataMaps,
207-
); // TODO move to parser/adapter, extending the one standard
208-
api.freeze(); // !! freeze and add parent !!
209-
210199
const completionList: CompletionList = {
211200
items: [],
212201
isIncomplete: false,

packages/apidom-ls/src/services/definition/definition-service.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { DefinitionParams, ReferenceParams } from 'vscode-languageserver-protoco
66
import { jsonPointerEvaluate } from '@swagger-api/apidom-reference';
77

88
import { LanguageSettings } from '../../apidom-language-types';
9-
import { getParser, isAsyncDoc } from '../../parser-factory';
10-
import { getSourceMap, isArray, isMember, isObject, setMetadataMap } from '../../utils/utils';
9+
import { getSourceMap, isArray, isMember, isObject } from '../../utils/utils';
1110

1211
export interface DefinitionService {
1312
doProvideDefinition(
@@ -35,26 +34,15 @@ export class DefaultDefinitionService implements DefinitionService {
3534
textDocument: TextDocument,
3635
definitionParams: DefinitionParams,
3736
): Promise<Location | null> {
38-
const parser = getParser(textDocument);
39-
const text: string = textDocument.getText();
40-
4137
const offset = textDocument.offsetAt(definitionParams.position);
4238

43-
const result = await parser.parse(text, { sourceMap: true });
44-
39+
const result = await this.settings!.documentCache?.get(textDocument);
40+
if (!result) return null;
4541
const api: ObjectElement = <ObjectElement>result.api;
4642

4743
// no API document has been parsed
4844
if (api === undefined) return null;
4945

50-
// use the type related metadata at root level
51-
setMetadataMap(
52-
api,
53-
isAsyncDoc(text) ? 'asyncapi' : 'openapi',
54-
this.settings?.metadata?.metadataMaps,
55-
); // TODO (francesco.tumanischvili@smartbear.com) move to parser/adapter, extending the one standard
56-
api.freeze(); // !! freeze and add parent !!
57-
5846
// TODO (francesco.tumanischvili@smartbear.com): handle by predicates and adapters, look for
5947
// refElements and/or metadata, replace current shaky handling by `$ref` key lookup
6048
const node = findAtOffset({ offset, includeRightBound: true }, api);
@@ -94,27 +82,16 @@ export class DefaultDefinitionService implements DefinitionService {
9482
textDocument: TextDocument,
9583
referenceParams: ReferenceParams,
9684
): Promise<Location[] | null> {
97-
const parser = getParser(textDocument);
98-
const text: string = textDocument.getText();
99-
10085
// const asyncapi: boolean = isAsyncDoc(textDocument);
10186
const offset = textDocument.offsetAt(referenceParams.position);
10287

103-
const result = await parser.parse(text, { sourceMap: true });
104-
88+
const result = await this.settings!.documentCache?.get(textDocument);
89+
if (!result) return null;
10590
const api: ObjectElement = <ObjectElement>result.api;
10691

10792
// no API document has been parsed
10893
if (api === undefined) return null;
10994

110-
// use the type related metadata at root level
111-
setMetadataMap(
112-
api,
113-
isAsyncDoc(text) ? 'asyncapi' : 'openapi',
114-
this.settings?.metadata?.metadataMaps,
115-
); // TODO (francesco.tumanischvili@smartbear.com) move to parser/adapter, extending the one standard
116-
api.freeze(); // !! freeze and add parent !!
117-
11895
// TODO(francesco.tumanischvili@smartbear.com): handle by predicates and adapters, look for
11996
// refElements and/or metadata, replace current shaky handling by `$ref` key lookup
12097
const node = findAtOffset({ offset, includeRightBound: true }, api);

packages/apidom-ls/src/services/hover/hover-service.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MarkupContent, Position, Range } from 'vscode-languageserver-types';
55

66
import { LanguageSettings } from '../../apidom-language-types';
77
import { getSourceMap, isMember, isObject, isArray, setMetadataMap } from '../../utils/utils';
8-
import { getParser, isAsyncDoc } from '../../parser-factory';
8+
import { isAsyncDoc } from '../../parser-factory';
99

1010
export interface HoverService {
1111
computeHover(textDocument: TextDocument, position: Position): Promise<Hover | undefined>;
@@ -27,7 +27,6 @@ export class DefaultHoverService implements HoverService {
2727
textDocument: TextDocument,
2828
position: Position,
2929
): Promise<Hover | undefined> {
30-
const parser = getParser(textDocument);
3130
const text: string = textDocument.getText();
3231
const asyncapi: boolean = isAsyncDoc(textDocument);
3332
const offset = textDocument.offsetAt(position);
@@ -36,9 +35,9 @@ export class DefaultHoverService implements HoverService {
3635
contents: { kind: 'markdown', value: '' },
3736
};
3837

39-
// parse
40-
const { api } = await parser.parse(text, { sourceMap: true });
41-
38+
const result = await this.settings!.documentCache?.get(textDocument);
39+
if (!result) return undefined;
40+
const { api } = result;
4241
// no API document has been parsed
4342
if (api === undefined) return undefined;
4443

0 commit comments

Comments
 (0)