Skip to content

Commit dbb2956

Browse files
committed
Discard CST to preserve memory
1 parent 77d16cf commit dbb2956

26 files changed

+304
-152
lines changed

examples/requirements/test/validator.test.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import { NodeFileSystem } from 'langium/node';
1313
describe('A requirement identifier and a test identifier shall contain a number.', () => {
1414
test('T001_good_case', async () => {
1515
const services = createRequirementsAndTestsLangServices(NodeFileSystem);
16-
const [mainDoc,allDocs] = await extractDocuments(
16+
const [mainDoc, allDocs] = await extractDocuments(
1717
path.join(__dirname, 'files', 'good', 'requirements.req'),
1818
services.requirements
1919
);
2020
expect((mainDoc.diagnostics ?? [])).toEqual([]);
2121
expect(allDocs.length).toEqual(3);
22-
allDocs.forEach(doc=>{
22+
allDocs.forEach(doc => {
2323
expect((doc.diagnostics ?? [])).toEqual([]);
2424
});
2525
});
@@ -35,7 +35,7 @@ describe('A requirement identifier shall contain a number.', () => {
3535
expect(mainDoc.diagnostics ?? []).toEqual(expect.arrayContaining([
3636
expect.objectContaining({
3737
message: expect.stringMatching('Requirement name ReqIdABC_reqID should container a number'),
38-
range: expect.objectContaining({start:expect.objectContaining({line: 2})}) // zero based
38+
range: expect.objectContaining({ start: expect.objectContaining({ line: 2 }) }) // zero based
3939
})
4040
]));
4141

@@ -45,17 +45,17 @@ describe('A requirement identifier shall contain a number.', () => {
4545
describe('A test identifier shall contain a number.', () => {
4646
test('T003_badTstId: bad case', async () => {
4747
const services = createRequirementsAndTestsLangServices(NodeFileSystem);
48-
const [,allDocs] = await extractDocuments(
48+
const [, allDocs] = await extractDocuments(
4949
path.join(__dirname, 'files', 'bad1', 'requirements.req'),
5050
services.requirements
5151
);
52-
const doc = allDocs.find(doc=>/tests_part1.tst/.test(doc.uri.fsPath));
52+
const doc = allDocs.find(doc => /tests_part1.tst/.test(doc.uri.fsPath));
5353
expect(doc).toBeDefined();
5454
if (!doc) throw new Error('impossible');
5555
expect(doc.diagnostics ?? []).toEqual(expect.arrayContaining([
5656
expect.objectContaining({
5757
message: expect.stringMatching('Test name TA should container a number.'),
58-
range: expect.objectContaining({start:expect.objectContaining({line: 1})}) // zero based
58+
range: expect.objectContaining({ start: expect.objectContaining({ line: 1 }) }) // zero based
5959
})
6060
]));
6161
});
@@ -71,7 +71,7 @@ describe('A requirement shall be covered by at least one test.', () => {
7171
expect(mainDoc.diagnostics ?? []).toEqual(expect.arrayContaining([
7272
expect.objectContaining({
7373
message: expect.stringMatching('Requirement ReqId004_unicorn not covered by a test.'),
74-
range: expect.objectContaining({start:expect.objectContaining({line: 4})}) // zero based
74+
range: expect.objectContaining({ start: expect.objectContaining({ line: 4 }) }) // zero based
7575
})
7676
]));
7777
});
@@ -80,28 +80,32 @@ describe('A requirement shall be covered by at least one test.', () => {
8080
describe('A referenced environment in a test must be found in one of the referenced requirements.', () => {
8181
test('referenced environment test', async () => {
8282
const services = createRequirementsAndTestsLangServices(NodeFileSystem);
83-
const [,allDocs] = await extractDocuments(
83+
const [, allDocs] = await extractDocuments(
8484
path.join(__dirname, 'files', 'bad2', 'requirements.req'),
8585
services.requirements
8686
);
87-
const doc = allDocs.find(doc=>/tests_part1.tst/.test(doc.uri.fsPath));
87+
const doc = allDocs.find(doc => /tests_part1.tst/.test(doc.uri.fsPath));
8888
expect(doc).toBeDefined();
8989
if (!doc) throw new Error('impossible');
9090
expect((doc.diagnostics ?? [])).toEqual(expect.arrayContaining([
9191
expect.objectContaining({
9292
message: expect.stringMatching('Test T002_badReqId references environment Linux_x86 which is used in any referenced requirement.'),
93-
range: expect.objectContaining({start:expect.objectContaining({
94-
line: 3,
95-
character: 65
96-
})}) // zero based
93+
range: expect.objectContaining({
94+
start: expect.objectContaining({
95+
line: 3,
96+
character: 65
97+
})
98+
}) // zero based
9799
})
98100
]));
99101
expect((doc.diagnostics ?? [])).toEqual(expect.arrayContaining([
100102
expect.objectContaining({
101103
message: expect.stringMatching('Test T004_cov references environment Linux_x86 which is used in any referenced requirement.'),
102-
range: expect.objectContaining({start:expect.objectContaining({
103-
line: 5
104-
})}) // zero based
104+
range: expect.objectContaining({
105+
start: expect.objectContaining({
106+
line: 5
107+
})
108+
}) // zero based
105109
})
106110
]));
107111

packages/langium/src/default-module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { LangiumParserErrorMessageProvider } from './parser/langium-parser.js';
3535
import { DefaultAsyncParser } from './parser/async-parser.js';
3636
import { DefaultWorkspaceLock } from './workspace/workspace-lock.js';
3737
import { DefaultHydrator } from './serializer/hydrator.js';
38+
import { DefaultEnvironment } from './workspace/environment.js';
3839

3940
/**
4041
* Context required for creating the default language-specific dependency injection module.
@@ -117,7 +118,8 @@ export function createDefaultSharedCoreModule(context: DefaultSharedCoreModuleCo
117118
WorkspaceManager: (services) => new DefaultWorkspaceManager(services),
118119
FileSystemProvider: (services) => context.fileSystemProvider(services),
119120
WorkspaceLock: () => new DefaultWorkspaceLock(),
120-
ConfigurationProvider: (services) => new DefaultConfigurationProvider(services)
121+
ConfigurationProvider: (services) => new DefaultConfigurationProvider(services),
122+
Environment: () => new DefaultEnvironment()
121123
}
122124
};
123125
}

packages/langium/src/documentation/comment-provider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ export class DefaultCommentProvider implements CommentProvider {
2828
this.grammarConfig = () => services.parser.GrammarConfig;
2929
}
3030
getComment(node: AstNode): string | undefined {
31-
if(isAstNodeWithComment(node)) {
31+
if (isAstNodeWithComment(node)) {
3232
return node.$comment;
33+
} else if (node.$segments && 'comment' in node.$segments) {
34+
return node.$segments.comment;
35+
} else {
36+
return findCommentNode(node.$cstNode, this.grammarConfig().multilineCommentRules)?.text;
3337
}
34-
return findCommentNode(node.$cstNode, this.grammarConfig().multilineCommentRules)?.text;
3538
}
3639
}

packages/langium/src/lsp/call-hierarchy-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ export abstract class AbstractCallHierarchyProvider implements CallHierarchyProv
5454
return undefined;
5555
}
5656

57-
const declarationNode = this.references.findDeclarationNode(targetNode);
57+
const declarationNode = this.references.findDeclaration(targetNode);
5858
if (!declarationNode) {
5959
return undefined;
6060
}
6161

62-
return this.getCallHierarchyItems(declarationNode.astNode, document);
62+
return this.getCallHierarchyItems(declarationNode, document);
6363
}
6464

6565
protected getCallHierarchyItems(targetNode: AstNode, document: LangiumDocument<AstNode>): CallHierarchyItem[] | undefined {

packages/langium/src/lsp/definition-provider.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { GrammarConfig } from '../languages/grammar-config.js';
1010
import type { NameProvider } from '../references/name-provider.js';
1111
import type { References } from '../references/references.js';
1212
import type { LangiumServices } from './lsp-services.js';
13-
import type { CstNode } from '../syntax-tree.js';
13+
import type { AstNode, CstNode } from '../syntax-tree.js';
1414
import type { MaybePromise } from '../utils/promise-utils.js';
1515
import type { LangiumDocument } from '../workspace/documents.js';
1616
import { LocationLink } from 'vscode-languageserver';
@@ -37,7 +37,7 @@ export interface DefinitionProvider {
3737

3838
export interface GoToLink {
3939
source: CstNode
40-
target: CstNode
40+
target: AstNode
4141
targetDocument: LangiumDocument
4242
}
4343

@@ -67,21 +67,25 @@ export class DefaultDefinitionProvider implements DefinitionProvider {
6767

6868
protected collectLocationLinks(sourceCstNode: CstNode, _params: DefinitionParams): MaybePromise<LocationLink[] | undefined> {
6969
const goToLink = this.findLink(sourceCstNode);
70-
if (goToLink) {
71-
return [LocationLink.create(
72-
goToLink.targetDocument.textDocument.uri,
73-
(goToLink.target.astNode.$cstNode ?? goToLink.target).range,
74-
goToLink.target.range,
75-
goToLink.source.range
76-
)];
70+
if (goToLink && goToLink.target.$segments) {
71+
const name = this.nameProvider.getNameProperty(goToLink.target);
72+
if (name) {
73+
const nameSegment = goToLink.target.$segments.properties.get(name);
74+
return nameSegment.map(segment => LocationLink.create(
75+
goToLink.targetDocument.textDocument.uri,
76+
goToLink.target.$segments!.full.range,
77+
segment.range,
78+
goToLink.source.range
79+
));
80+
}
7781
}
7882
return undefined;
7983
}
8084

8185
protected findLink(source: CstNode): GoToLink | undefined {
82-
const target = this.references.findDeclarationNode(source);
83-
if (target?.astNode) {
84-
const targetDocument = getDocument(target.astNode);
86+
const target = this.references.findDeclaration(source);
87+
if (target) {
88+
const targetDocument = getDocument(target);
8589
if (target && targetDocument) {
8690
return { source, target, targetDocument };
8791
}

packages/langium/src/lsp/document-update-handler.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
* terms of the MIT License, which is available in the project root.
55
******************************************************************************/
66

7-
import type { TextDocumentWillSaveEvent, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, TextDocumentChangeEvent, TextEdit } from 'vscode-languageserver';
7+
import type { TextDocumentWillSaveEvent, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, TextDocumentChangeEvent, TextEdit, Connection } from 'vscode-languageserver';
88
import { DidChangeWatchedFilesNotification, FileChangeType } from 'vscode-languageserver';
99
import { stream } from '../utils/stream.js';
1010
import { URI } from '../utils/uri-utils.js';
1111
import type { DocumentBuilder } from '../workspace/document-builder.js';
12-
import type { TextDocument } from '../workspace/documents.js';
12+
import type { LangiumDocuments, TextDocument } from '../workspace/documents.js';
1313
import type { WorkspaceLock } from '../workspace/workspace-lock.js';
1414
import type { LangiumSharedServices } from './lsp-services.js';
1515
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
1616
import type { ServiceRegistry } from '../service-registry.js';
1717
import type { MaybePromise } from '../utils/promise-utils.js';
18+
import { discardCst } from '../utils/cst-utils.js';
1819

1920
/**
2021
* Shared service for handling text document changes and watching relevant files.
@@ -71,13 +72,17 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
7172
protected readonly workspaceManager: WorkspaceManager;
7273
protected readonly documentBuilder: DocumentBuilder;
7374
protected readonly workspaceLock: WorkspaceLock;
75+
protected readonly documents: LangiumDocuments;
76+
protected readonly connection: Connection | undefined;
7477
protected readonly serviceRegistry: ServiceRegistry;
7578

7679
constructor(services: LangiumSharedServices) {
7780
this.workspaceManager = services.workspace.WorkspaceManager;
7881
this.documentBuilder = services.workspace.DocumentBuilder;
7982
this.workspaceLock = services.workspace.WorkspaceLock;
8083
this.serviceRegistry = services.ServiceRegistry;
84+
this.documents = services.workspace.LangiumDocuments;
85+
this.connection = services.lsp.Connection;
8186

8287
let canRegisterFileWatcher = false;
8388
services.lsp.LanguageServer.onInitialize(params => {
@@ -98,15 +103,14 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
98103
.distinct()
99104
.toArray();
100105
if (fileExtensions.length > 0) {
101-
const connection = services.lsp.Connection;
102106
const options: DidChangeWatchedFilesRegistrationOptions = {
103107
watchers: [{
104108
globPattern: fileExtensions.length === 1
105109
? `**/*.${fileExtensions[0]}`
106110
: `**/*.{${fileExtensions.join(',')}}`
107111
}]
108112
};
109-
connection?.client.register(DidChangeWatchedFilesNotification.type, options);
113+
this.connection?.client.register(DidChangeWatchedFilesNotification.type, options);
110114
}
111115
}
112116

@@ -141,4 +145,18 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
141145
.toArray();
142146
this.fireDocumentUpdate(changedUris, deletedUris);
143147
}
148+
149+
didCloseDocument(event: TextDocumentChangeEvent<TextDocument>): void {
150+
const document = this.documents.getDocument(URI.parse(event.document.uri));
151+
if (document) {
152+
// Preserve memory by discarding the CST of the document
153+
// Whenever the user reopens the document, the CST will be rebuilt
154+
discardCst(document.parseResult.value);
155+
}
156+
// Discard the diagnostics for the closed document
157+
this.connection?.sendDiagnostics({
158+
uri: event.document.uri,
159+
diagnostics: []
160+
});
161+
}
144162
}

packages/langium/src/lsp/language-server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export class DefaultLanguageServer implements LanguageServer {
198198
}
199199

200200
protected fireInitializeOnDefaultServices(params: InitializeParams): void {
201+
this.services.workspace.Environment.initialize(params);
201202
this.services.workspace.ConfigurationProvider.initialize(params);
202203
this.services.workspace.WorkspaceManager.initialize(params);
203204
}

packages/langium/src/lsp/type-hierarchy-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ export abstract class AbstractTypeHierarchyProvider implements TypeHierarchyProv
5858
return undefined;
5959
}
6060

61-
const declarationNode = this.references.findDeclarationNode(targetNode);
61+
const declarationNode = this.references.findDeclaration(targetNode);
6262
if (!declarationNode) {
6363
return undefined;
6464
}
6565

66-
return this.getTypeHierarchyItems(declarationNode.astNode, document);
66+
return this.getTypeHierarchyItems(declarationNode, document);
6767
}
6868

6969
protected getTypeHierarchyItems(targetNode: AstNode, document: LangiumDocument): TypeHierarchyItem[] | undefined {

packages/langium/src/parser/async-parser.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import type { CancellationToken } from '../utils/cancellation.js';
88
import type { LangiumCoreServices } from '../services.js';
99
import type { AstNode } from '../syntax-tree.js';
10-
import type { LangiumParser, ParseResult } from './langium-parser.js';
10+
import type { LangiumParser, ParseResult, ParserOptions } from './langium-parser.js';
1111
import type { Hydrator } from '../serializer/hydrator.js';
1212
import type { Event } from '../utils/event.js';
1313
import { Deferred, OperationCancelled } from '../utils/promise-utils.js';
@@ -30,7 +30,7 @@ export interface AsyncParser {
3030
*
3131
* @throws `OperationCancelled` if the parsing process is cancelled.
3232
*/
33-
parse<T extends AstNode>(text: string, cancelToken: CancellationToken): Promise<ParseResult<T>>;
33+
parse<T extends AstNode>(text: string, options: ParserOptions | undefined, cancelToken: CancellationToken): Promise<ParseResult<T>>;
3434
}
3535

3636
/**
@@ -47,8 +47,8 @@ export class DefaultAsyncParser implements AsyncParser {
4747
this.syncParser = services.parser.LangiumParser;
4848
}
4949

50-
parse<T extends AstNode>(text: string, _cancelToken: CancellationToken): Promise<ParseResult<T>> {
51-
return Promise.resolve(this.syncParser.parse<T>(text));
50+
parse<T extends AstNode>(text: string, options: ParserOptions | undefined, _cancelToken: CancellationToken): Promise<ParseResult<T>> {
51+
return Promise.resolve(this.syncParser.parse<T>(text, options));
5252
}
5353
}
5454

@@ -89,7 +89,7 @@ export abstract class AbstractThreadedAsyncParser implements AsyncParser {
8989
}
9090
}
9191

92-
async parse<T extends AstNode>(text: string, cancelToken: CancellationToken): Promise<ParseResult<T>> {
92+
async parse<T extends AstNode>(text: string, options: ParserOptions | undefined, cancelToken: CancellationToken): Promise<ParseResult<T>> {
9393
const worker = await this.acquireParserWorker(cancelToken);
9494
const deferred = new Deferred<ParseResult<T>>();
9595
let timeout: NodeJS.Timeout | undefined;
@@ -101,7 +101,7 @@ export abstract class AbstractThreadedAsyncParser implements AsyncParser {
101101
this.terminateWorker(worker);
102102
}, this.terminationDelay);
103103
});
104-
worker.parse(text).then(result => {
104+
worker.parse(text, options).then(result => {
105105
const hydrated = this.hydrator.hydrate<T>(result);
106106
deferred.resolve(hydrated);
107107
}).catch(err => {
@@ -194,13 +194,13 @@ export class ParserWorker {
194194
this.onReadyEmitter.fire();
195195
}
196196

197-
parse(text: string): Promise<ParseResult> {
197+
parse(text: string, options: ParserOptions | undefined): Promise<ParseResult> {
198198
if (this._parsing) {
199199
throw new Error('Parser worker is busy');
200200
}
201201
this._parsing = true;
202202
this.deferred = new Deferred();
203-
this.sendMessage(text);
203+
this.sendMessage([text, options]);
204204
return this.deferred.promise;
205205
}
206206
}

packages/langium/src/parser/cst-node-builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,20 @@ export class CstNodeBuilder {
8787
}
8888
}
8989

90-
construct(item: { $type: string | symbol | undefined, $cstNode: CstNode }): void {
90+
construct(item: { $type: string | symbol | undefined, $cstNode: CstNode }): CstNode {
9191
const current: CstNode = this.current;
9292
// The specified item could be a datatype ($type is symbol) or a fragment ($type is undefined)
9393
// Only if the $type is a string, we actually assign the element
9494
if (typeof item.$type === 'string') {
9595
this.current.astNode = <AstNode>item;
9696
}
97-
item.$cstNode = current;
9897
const node = this.nodeStack.pop();
9998
// Empty composite nodes are not valid
10099
// Simply remove the node from the tree
101100
if (node?.content.length === 0) {
102101
this.removeNode(node);
103102
}
103+
return current;
104104
}
105105
}
106106

0 commit comments

Comments
 (0)