Skip to content

Commit 5104b23

Browse files
committed
Support comments in closed files
1 parent 4ec3f63 commit 5104b23

File tree

6 files changed

+69
-30
lines changed

6 files changed

+69
-30
lines changed

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/parser/cst-node-builder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export class CstNodeBuilder {
3737
return compositeNode;
3838
}
3939

40-
buildLeafNode(token: IToken, feature: AbstractElement): LeafCstNode {
41-
const leafNode = new LeafCstNodeImpl(token.startOffset, token.image.length, tokenToRange(token), token.tokenType, false);
40+
buildLeafNode(token: IToken, feature?: AbstractElement): LeafCstNode {
41+
const leafNode = new LeafCstNodeImpl(token.startOffset, token.image.length, tokenToRange(token), token.tokenType, !feature);
4242
leafNode.grammarSource = feature;
4343
leafNode.root = this.rootNode;
4444
this.current.content.push(leafNode);
@@ -107,7 +107,7 @@ export abstract class AbstractCstNode implements CstNode {
107107
abstract get range(): Range;
108108

109109
container?: CompositeCstNode;
110-
grammarSource: AbstractElement;
110+
grammarSource?: AbstractElement;
111111
root: RootCstNode;
112112
private _astNode?: AstNode;
113113

@@ -117,7 +117,7 @@ export abstract class AbstractCstNode implements CstNode {
117117
}
118118

119119
/** @deprecated use `grammarSource` instead. */
120-
get feature(): AbstractElement {
120+
get feature(): AbstractElement | undefined {
121121
return this.grammarSource;
122122
}
123123

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { AbstractElement, Action, Assignment, ParserRule } from '../languag
1010
import type { Linker } from '../references/linker.js';
1111
import type { LangiumCoreServices } from '../services.js';
1212
import type { AstNode, AstReflection, CompositeCstNode, CstNode } from '../syntax-tree.js';
13-
import type { Lexer } from './lexer.js';
13+
import type { Lexer, LexerResult } from './lexer.js';
1414
import type { IParserConfig } from './parser-config.js';
1515
import type { ValueConverter } from './value-converter.js';
1616
import { defaultParserErrorProvider, EmbeddedActionsParser, LLkLookaheadStrategy } from 'chevrotain';
@@ -21,6 +21,7 @@ import { assignMandatoryProperties, getContainerOfType, linkContentToContainer }
2121
import { CstNodeBuilder } from './cst-node-builder.js';
2222
import type { LexingReport } from './token-builder.js';
2323
import { toDocumentSegment } from '../utils/cst-utils.js';
24+
import type { CommentProvider } from '../documentation/comment-provider.js';
2425

2526
export type ParseResult<T = AstNode> = {
2627
value: T,
@@ -123,6 +124,7 @@ const withRuleSuffix = (name: string): string => name.endsWith(ruleSuffix) ? nam
123124
export abstract class AbstractLangiumParser implements BaseParser {
124125

125126
protected readonly lexer: Lexer;
127+
protected readonly commentProvider: CommentProvider;
126128
protected readonly wrapper: ChevrotainWrapper;
127129
protected _unorderedGroups: Map<string, boolean[]> = new Map<string, boolean[]>();
128130

@@ -138,6 +140,7 @@ export abstract class AbstractLangiumParser implements BaseParser {
138140
skipValidations: production,
139141
errorMessageProvider: services.parser.ParserErrorMessageProvider
140142
});
143+
this.commentProvider = services.documentation.CommentProvider;
141144
}
142145

143146
alternatives(idx: number, choices: Array<IOrAlt<any>>): void {
@@ -197,6 +200,7 @@ export class LangiumParser extends AbstractLangiumParser {
197200
private readonly converter: ValueConverter;
198201
private readonly astReflection: AstReflection;
199202
private readonly nodeBuilder = new CstNodeBuilder();
203+
private lexerResult?: LexerResult;
200204
private stack: any[] = [];
201205
private assignmentMap = new Map<AbstractElement, AssignmentElement | undefined>();
202206
private currentMode: CstParserMode = CstParserMode.Retain;
@@ -236,17 +240,15 @@ export class LangiumParser extends AbstractLangiumParser {
236240
parse<T extends AstNode = AstNode>(input: string, options: ParserOptions = {}): ParseResult<T> {
237241
this.currentMode = options.cst ?? CstParserMode.Retain;
238242
this.nodeBuilder.buildRootNode(input);
239-
const lexerResult = this.lexer.tokenize(input);
243+
const lexerResult = this.lexerResult = this.lexer.tokenize(input);
240244
this.wrapper.input = lexerResult.tokens;
241245
const ruleMethod = options.rule ? this.allRules.get(options.rule) : this.mainRule;
242246
if (!ruleMethod) {
243247
throw new Error(options.rule ? `No rule found with name '${options.rule}'` : 'No main rule available.');
244248
}
245249
const result = ruleMethod.call(this.wrapper, {});
246-
if (this.currentMode === CstParserMode.Retain) {
247-
this.nodeBuilder.addHiddenTokens(lexerResult.hidden);
248-
}
249250
this.unorderedGroups.clear();
251+
this.lexerResult = undefined;
250252
return {
251253
value: result,
252254
lexerErrors: lexerResult.errors,
@@ -277,9 +279,32 @@ export class LangiumParser extends AbstractLangiumParser {
277279
};
278280
}
279281

282+
private appendHiddenTokens(tokens: IToken[]): void {
283+
for (const token of tokens) {
284+
this.nodeBuilder.buildLeafNode(token);
285+
}
286+
}
287+
288+
private getHiddenTokens(token: IToken): IToken[] {
289+
const hiddenTokens = this.lexerResult!.hidden;
290+
if (!hiddenTokens.length) {
291+
return [];
292+
}
293+
const offset = token.startOffset;
294+
for (let i = 0; i < hiddenTokens.length; i++) {
295+
const token = hiddenTokens[i];
296+
if (token.startOffset > offset) {
297+
return hiddenTokens.splice(0, i);
298+
}
299+
}
300+
return hiddenTokens.splice(0, hiddenTokens.length);
301+
}
302+
280303
consume(idx: number, tokenType: TokenType, feature: AbstractElement): void {
281304
const token = this.wrapper.wrapConsume(idx, tokenType);
282305
if (!this.isRecording() && this.isValidToken(token)) {
306+
const hiddenTokens = this.getHiddenTokens(token);
307+
this.appendHiddenTokens(hiddenTokens);
283308
const leafNode = this.nodeBuilder.buildLeafNode(token, feature);
284309
const { assignment, isCrossRef } = this.getAssignment(feature);
285310
const current = this.current;
@@ -374,9 +399,12 @@ export class LangiumParser extends AbstractLangiumParser {
374399
} else {
375400
assignMandatoryProperties(this.astReflection, obj);
376401
}
402+
obj.$cstNode = cstNode;
403+
delete obj.$segments.comment;
404+
obj.$segments.comment = this.commentProvider.getComment(obj);
377405
obj.$segments.full = toDocumentSegment(cstNode);
378-
if (this.currentMode === CstParserMode.Retain) {
379-
obj.$cstNode = cstNode;
406+
if (this.currentMode === CstParserMode.Discard) {
407+
obj.$cstNode = undefined;
380408
}
381409
return [obj, cstNode];
382410
}

packages/langium/src/serializer/hydrator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,13 @@ export class DefaultHydrator implements Hydrator {
296296
return this.lexer.definition[name];
297297
}
298298

299-
protected getGrammarElementId(node: AbstractElement): number | undefined {
299+
protected getGrammarElementId(node: AbstractElement | undefined): number | undefined {
300300
if (this.grammarElementIdMap.size === 0) {
301301
this.createGrammarElementIdMap();
302302
}
303+
if (!node) {
304+
return undefined;
305+
}
303306
return this.grammarElementIdMap.get(node);
304307
}
305308

packages/langium/src/syntax-tree.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type Properties<N extends AstNode> = SpecificNodeProperties<N> extends ne
4545

4646
export interface AstNodeSegments {
4747
readonly full: DocumentSegment;
48+
readonly comment?: string;
4849
readonly properties: Record<string, DocumentSegment[]>;
4950
}
5051

@@ -240,9 +241,9 @@ export interface CstNode extends DocumentSegment {
240241
/** The root CST node */
241242
readonly root: RootCstNode;
242243
/** The grammar element from which this node was parsed */
243-
readonly grammarSource: AbstractElement;
244+
readonly grammarSource?: AbstractElement;
244245
/** @deprecated use `grammarSource` instead. */
245-
readonly feature: AbstractElement;
246+
readonly feature?: AbstractElement;
246247
/** The AST node created from this CST node */
247248
readonly astNode: AstNode;
248249
/** @deprecated use `astNode` instead. */

packages/langium/src/utils/cst-utils.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Range } from 'vscode-languageserver-types';
99
import type { CstNode, CompositeCstNode, LeafCstNode, AstNode, Mutable, Reference } from '../syntax-tree.js';
1010
import type { DocumentSegment } from '../workspace/documents.js';
1111
import type { Stream, TreeStream } from './stream.js';
12-
import { isCompositeCstNode, isLeafCstNode, isRootCstNode } from '../syntax-tree.js';
12+
import { isCompositeCstNode, isLeafCstNode } from '../syntax-tree.js';
1313
import { TreeStreamImpl } from './stream.js';
1414
import { streamAst, streamReferences } from './ast-utils.js';
1515

@@ -135,20 +135,24 @@ export function findDeclarationNodeAtOffset(cstNode: CstNode | undefined, offset
135135
}
136136

137137
export function findCommentNode(cstNode: CstNode | undefined, commentNames: string[]): CstNode | undefined {
138-
if (cstNode) {
139-
const previous = getPreviousNode(cstNode, true);
140-
if (previous && isCommentNode(previous, commentNames)) {
141-
return previous;
138+
if (isCompositeCstNode(cstNode)) {
139+
const firstChild = cstNode.content[0];
140+
if (isCompositeCstNode(firstChild)) {
141+
return findCommentNode(firstChild, commentNames);
142+
}
143+
// Find the first non-hidden child
144+
let index = -1;
145+
for (const child of cstNode.content) {
146+
if (!child.hidden) {
147+
break;
148+
}
149+
index++;
142150
}
143-
if (isRootCstNode(cstNode)) {
144-
// Go from the first non-hidden node through all nodes in reverse order
145-
// We do this to find the comment node which directly precedes the root node
146-
const endIndex = cstNode.content.findIndex(e => !e.hidden);
147-
for (let i = endIndex - 1; i >= 0; i--) {
148-
const child = cstNode.content[i];
149-
if (isCommentNode(child, commentNames)) {
150-
return child;
151-
}
151+
// Search backwards for a comment node
152+
for (let i = index; i >= 0; i--) {
153+
const child = cstNode.content[i];
154+
if (isCommentNode(child, commentNames)) {
155+
return child;
152156
}
153157
}
154158
}

0 commit comments

Comments
 (0)