@nowline/lsp 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +49 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/nowline-lsp-module.d.ts +29 -0
- package/dist/nowline-lsp-module.d.ts.map +1 -0
- package/dist/nowline-lsp-module.js +49 -0
- package/dist/nowline-lsp-module.js.map +1 -0
- package/dist/providers/completion.d.ts +28 -0
- package/dist/providers/completion.d.ts.map +1 -0
- package/dist/providers/completion.js +175 -0
- package/dist/providers/completion.js.map +1 -0
- package/dist/providers/definition.d.ts +18 -0
- package/dist/providers/definition.d.ts.map +1 -0
- package/dist/providers/definition.js +39 -0
- package/dist/providers/definition.js.map +1 -0
- package/dist/providers/document-symbols.d.ts +24 -0
- package/dist/providers/document-symbols.d.ts.map +1 -0
- package/dist/providers/document-symbols.js +126 -0
- package/dist/providers/document-symbols.js.map +1 -0
- package/dist/providers/hover.d.ts +18 -0
- package/dist/providers/hover.d.ts.map +1 -0
- package/dist/providers/hover.js +78 -0
- package/dist/providers/hover.js.map +1 -0
- package/dist/providers/references.d.ts +19 -0
- package/dist/providers/references.d.ts.map +1 -0
- package/dist/providers/references.js +70 -0
- package/dist/providers/references.js.map +1 -0
- package/dist/providers/rename.d.ts +16 -0
- package/dist/providers/rename.d.ts.map +1 -0
- package/dist/providers/rename.js +72 -0
- package/dist/providers/rename.js.map +1 -0
- package/dist/references/ast-utils.d.ts +126 -0
- package/dist/references/ast-utils.d.ts.map +1 -0
- package/dist/references/ast-utils.js +313 -0
- package/dist/references/ast-utils.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +35 -0
- package/dist/server.js.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +26 -0
- package/src/nowline-lsp-module.ts +102 -0
- package/src/providers/completion.ts +202 -0
- package/src/providers/definition.ts +58 -0
- package/src/providers/document-symbols.ts +152 -0
- package/src/providers/hover.ts +98 -0
- package/src/providers/references.ts +86 -0
- package/src/providers/rename.ts +109 -0
- package/src/references/ast-utils.ts +378 -0
- package/src/server.ts +33 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { MaybePromise } from 'langium';
|
|
2
|
+
import {
|
|
3
|
+
type CompletionAcceptor,
|
|
4
|
+
type CompletionContext,
|
|
5
|
+
DefaultCompletionProvider,
|
|
6
|
+
type NextFeature,
|
|
7
|
+
} from 'langium/lsp';
|
|
8
|
+
import { type CompletionItem, CompletionItemKind } from 'vscode-languageserver';
|
|
9
|
+
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
10
|
+
import {
|
|
11
|
+
BUILTIN_CAPACITY_ICONS,
|
|
12
|
+
BUILTIN_ICON_NAMES,
|
|
13
|
+
BUILTIN_STATUSES,
|
|
14
|
+
collectNamedEntities,
|
|
15
|
+
entityKind,
|
|
16
|
+
fileFromDocument,
|
|
17
|
+
type NamedEntity,
|
|
18
|
+
REFERENCE_PROP_KEYS,
|
|
19
|
+
} from '../references/ast-utils.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Per-key allowlist of entity kinds that make sense as values. Keeps
|
|
23
|
+
* suggestions focused — typing `style:` only shows styles instead of every
|
|
24
|
+
* declared id in the file.
|
|
25
|
+
*
|
|
26
|
+
* The string values match the output of `entityKind()`
|
|
27
|
+
* (`StyleDeclaration` → `style`, `ItemDeclaration` → `item`, …).
|
|
28
|
+
*/
|
|
29
|
+
const REF_KEY_TO_KINDS: Record<string, ReadonlySet<string>> = {
|
|
30
|
+
after: new Set(['item', 'milestone', 'anchor', 'parallel', 'group']),
|
|
31
|
+
before: new Set(['item', 'milestone', 'anchor', 'parallel', 'group']),
|
|
32
|
+
depends: new Set(['item', 'milestone', 'anchor', 'parallel', 'group']),
|
|
33
|
+
on: new Set(['item', 'swimlane', 'parallel', 'group', 'milestone']),
|
|
34
|
+
owner: new Set(['person', 'team']),
|
|
35
|
+
team: new Set(['team']),
|
|
36
|
+
style: new Set(['style']),
|
|
37
|
+
size: new Set(['size']),
|
|
38
|
+
status: new Set(['status']),
|
|
39
|
+
labels: new Set(['label']),
|
|
40
|
+
icon: new Set(['symbol']),
|
|
41
|
+
'capacity-icon': new Set(['symbol']),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Custom completion provider. Defers to Langium's keyword + property-key
|
|
46
|
+
* completion (free from the grammar) and adds:
|
|
47
|
+
*
|
|
48
|
+
* - Id-reference completion when the cursor sits in a reference property's
|
|
49
|
+
* value position (`after:`, `before:`, `style:`, `size:`, `labels:[...]`,
|
|
50
|
+
* etc.). Each key only suggests entities whose kind makes sense for it
|
|
51
|
+
* (see `REF_KEY_TO_KINDS`).
|
|
52
|
+
* - Status-value completion: built-in values plus custom `status`
|
|
53
|
+
* declarations.
|
|
54
|
+
* - Icon / capacity-icon built-in vocabulary plus user-declared symbols.
|
|
55
|
+
*/
|
|
56
|
+
export class NowlineCompletionProvider extends DefaultCompletionProvider {
|
|
57
|
+
constructor(services: NowlineLspServices) {
|
|
58
|
+
super(services);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected override completionFor(
|
|
62
|
+
context: CompletionContext,
|
|
63
|
+
next: NextFeature,
|
|
64
|
+
acceptor: CompletionAcceptor,
|
|
65
|
+
): MaybePromise<void> {
|
|
66
|
+
const valueHint = detectPropertyValueContext(context);
|
|
67
|
+
if (valueHint) {
|
|
68
|
+
this.acceptValueCompletions(context, valueHint, acceptor);
|
|
69
|
+
}
|
|
70
|
+
return super.completionFor(context, next, acceptor);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Public so subclasses (and tests) can extend the value-completion table
|
|
75
|
+
* without rewriting the dispatch logic.
|
|
76
|
+
*/
|
|
77
|
+
protected acceptValueCompletions(
|
|
78
|
+
context: CompletionContext,
|
|
79
|
+
hint: PropertyValueContext,
|
|
80
|
+
acceptor: CompletionAcceptor,
|
|
81
|
+
): void {
|
|
82
|
+
const file = fileFromDocument(context.document);
|
|
83
|
+
if (!file) return;
|
|
84
|
+
|
|
85
|
+
if (hint.key === 'status') {
|
|
86
|
+
for (const status of BUILTIN_STATUSES) {
|
|
87
|
+
acceptor(context, {
|
|
88
|
+
label: status,
|
|
89
|
+
kind: CompletionItemKind.EnumMember,
|
|
90
|
+
detail: 'built-in status',
|
|
91
|
+
insertText: status,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (hint.key === 'icon') {
|
|
96
|
+
for (const icon of BUILTIN_ICON_NAMES) {
|
|
97
|
+
acceptor(context, {
|
|
98
|
+
label: icon,
|
|
99
|
+
kind: CompletionItemKind.EnumMember,
|
|
100
|
+
detail: 'built-in icon',
|
|
101
|
+
insertText: icon,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (hint.key === 'capacity-icon') {
|
|
106
|
+
for (const icon of BUILTIN_CAPACITY_ICONS) {
|
|
107
|
+
acceptor(context, {
|
|
108
|
+
label: icon,
|
|
109
|
+
kind: CompletionItemKind.EnumMember,
|
|
110
|
+
detail: 'built-in capacity icon',
|
|
111
|
+
insertText: icon,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (REFERENCE_PROP_KEYS.has(hint.key)) {
|
|
117
|
+
const allowed = REF_KEY_TO_KINDS[hint.key];
|
|
118
|
+
for (const ent of collectNamedEntities(file)) {
|
|
119
|
+
if (!ent.name) continue;
|
|
120
|
+
if (allowed && !allowed.has(entityKind(ent))) continue;
|
|
121
|
+
acceptor(context, buildEntityItem(ent));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface PropertyValueContext {
|
|
128
|
+
key: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Best-effort detection of "we're typing inside a property value".
|
|
133
|
+
*
|
|
134
|
+
* The grammar tokenises `key:` as a single PROPERTY_KEY_WITH_COLON, so we
|
|
135
|
+
* walk the text backwards from the cursor over identifier characters, list
|
|
136
|
+
* separators, and `[` to find the closest `key:` token. If we hit `\n` or any
|
|
137
|
+
* other terminator before finding one, the cursor isn't inside a property
|
|
138
|
+
* value.
|
|
139
|
+
*
|
|
140
|
+
* Returns the property key (without the trailing colon) when a match is
|
|
141
|
+
* found.
|
|
142
|
+
*/
|
|
143
|
+
function detectPropertyValueContext(context: CompletionContext): PropertyValueContext | undefined {
|
|
144
|
+
const text = context.textDocument.getText();
|
|
145
|
+
let i = context.tokenOffset - 1;
|
|
146
|
+
while (i >= 0) {
|
|
147
|
+
const ch = text[i];
|
|
148
|
+
if (ch === ':') break;
|
|
149
|
+
if (ch === '\n' || ch === '\r') return undefined;
|
|
150
|
+
if (!/[A-Za-z0-9_\-,. \t[\]"'%/]/.test(ch)) return undefined;
|
|
151
|
+
i--;
|
|
152
|
+
}
|
|
153
|
+
if (i < 0) return undefined;
|
|
154
|
+
let j = i - 1;
|
|
155
|
+
while (j >= 0 && /[A-Za-z0-9_-]/.test(text[j])) j--;
|
|
156
|
+
const key = text.slice(j + 1, i);
|
|
157
|
+
if (!key) return undefined;
|
|
158
|
+
return { key };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildEntityItem(entity: NamedEntity): CompletionItem {
|
|
162
|
+
const kind = entityKind(entity);
|
|
163
|
+
return {
|
|
164
|
+
label: entity.name!,
|
|
165
|
+
kind: kindFor(kind),
|
|
166
|
+
detail: entity.title ? `${kind} — ${entity.title}` : kind,
|
|
167
|
+
insertText: entity.name!,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function kindFor(kind: string): CompletionItemKind {
|
|
172
|
+
switch (kind) {
|
|
173
|
+
case 'item':
|
|
174
|
+
return CompletionItemKind.Field;
|
|
175
|
+
case 'swimlane':
|
|
176
|
+
return CompletionItemKind.Class;
|
|
177
|
+
case 'parallel':
|
|
178
|
+
case 'group':
|
|
179
|
+
return CompletionItemKind.Module;
|
|
180
|
+
case 'anchor':
|
|
181
|
+
case 'milestone':
|
|
182
|
+
return CompletionItemKind.Event;
|
|
183
|
+
case 'person':
|
|
184
|
+
return CompletionItemKind.Constant;
|
|
185
|
+
case 'team':
|
|
186
|
+
return CompletionItemKind.Module;
|
|
187
|
+
case 'label':
|
|
188
|
+
return CompletionItemKind.EnumMember;
|
|
189
|
+
case 'size':
|
|
190
|
+
return CompletionItemKind.Value;
|
|
191
|
+
case 'status':
|
|
192
|
+
return CompletionItemKind.Enum;
|
|
193
|
+
case 'style':
|
|
194
|
+
return CompletionItemKind.Color;
|
|
195
|
+
case 'symbol':
|
|
196
|
+
return CompletionItemKind.Snippet;
|
|
197
|
+
case 'footnote':
|
|
198
|
+
return CompletionItemKind.Text;
|
|
199
|
+
default:
|
|
200
|
+
return CompletionItemKind.Reference;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LangiumDocument, MaybePromise } from 'langium';
|
|
2
|
+
import type { DefinitionProvider } from 'langium/lsp';
|
|
3
|
+
import type { CancellationToken, DefinitionParams } from 'vscode-languageserver';
|
|
4
|
+
import { LocationLink } from 'vscode-languageserver';
|
|
5
|
+
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
6
|
+
import {
|
|
7
|
+
buildEntityIndex,
|
|
8
|
+
fileFromDocument,
|
|
9
|
+
leafAt,
|
|
10
|
+
nameRangeOf,
|
|
11
|
+
propertyValueAt,
|
|
12
|
+
propKey,
|
|
13
|
+
REFERENCE_PROP_KEYS,
|
|
14
|
+
} from '../references/ast-utils.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom definition provider. The Nowline grammar uses `PropertyAtom` (plain
|
|
18
|
+
* text) for cross-references, so Langium's default cross-ref-aware provider
|
|
19
|
+
* has nothing to follow. We walk the AST instead: when the cursor sits on a
|
|
20
|
+
* value of a reference property (`after:`, `before:`, `owner:`, `on:`,
|
|
21
|
+
* `depends:[]`), look up the target id in the file-level entity index and
|
|
22
|
+
* return its declaration site.
|
|
23
|
+
*/
|
|
24
|
+
export class NowlineDefinitionProvider implements DefinitionProvider {
|
|
25
|
+
constructor(_services: NowlineLspServices) {
|
|
26
|
+
/* No collaborator dependencies; AST helpers are pure. */
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getDefinition(
|
|
30
|
+
document: LangiumDocument,
|
|
31
|
+
params: DefinitionParams,
|
|
32
|
+
_cancelToken?: CancellationToken,
|
|
33
|
+
): MaybePromise<LocationLink[] | undefined> {
|
|
34
|
+
const file = fileFromDocument(document);
|
|
35
|
+
if (!file) return undefined;
|
|
36
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
37
|
+
const leaf = leafAt(document, offset);
|
|
38
|
+
const hit = propertyValueAt(leaf);
|
|
39
|
+
if (!hit) return undefined;
|
|
40
|
+
if (!REFERENCE_PROP_KEYS.has(propKey(hit.prop))) return undefined;
|
|
41
|
+
|
|
42
|
+
const index = buildEntityIndex(file);
|
|
43
|
+
const target = index.get(hit.value);
|
|
44
|
+
if (!target) return undefined;
|
|
45
|
+
const targetCst = target.$cstNode;
|
|
46
|
+
if (!targetCst) return undefined;
|
|
47
|
+
|
|
48
|
+
const nameRange = nameRangeOf(target) ?? targetCst.range;
|
|
49
|
+
return [
|
|
50
|
+
LocationLink.create(
|
|
51
|
+
document.uri.toString(),
|
|
52
|
+
targetCst.range,
|
|
53
|
+
nameRange,
|
|
54
|
+
hit.valueNode.range,
|
|
55
|
+
),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type GroupBlock,
|
|
3
|
+
type ItemDeclaration,
|
|
4
|
+
isAnchorDeclaration,
|
|
5
|
+
isFootnoteDeclaration,
|
|
6
|
+
isGroupBlock,
|
|
7
|
+
isItemDeclaration,
|
|
8
|
+
isLabelDeclaration,
|
|
9
|
+
isMilestoneDeclaration,
|
|
10
|
+
isParallelBlock,
|
|
11
|
+
isPersonDeclaration,
|
|
12
|
+
isSizeDeclaration,
|
|
13
|
+
isStatusDeclaration,
|
|
14
|
+
isSwimlaneDeclaration,
|
|
15
|
+
isTeamDeclaration,
|
|
16
|
+
type ParallelBlock,
|
|
17
|
+
type RoadmapDeclaration,
|
|
18
|
+
type SwimlaneContent,
|
|
19
|
+
type SwimlaneDeclaration,
|
|
20
|
+
} from '@nowline/core';
|
|
21
|
+
import type { AstNode, LangiumDocument, MaybePromise } from 'langium';
|
|
22
|
+
import type { DocumentSymbolProvider } from 'langium/lsp';
|
|
23
|
+
import type {
|
|
24
|
+
CancellationToken,
|
|
25
|
+
DocumentSymbol,
|
|
26
|
+
DocumentSymbolParams,
|
|
27
|
+
Range,
|
|
28
|
+
} from 'vscode-languageserver';
|
|
29
|
+
import { SymbolKind } from 'vscode-languageserver';
|
|
30
|
+
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
31
|
+
import { entityKind, fileFromDocument, nameRangeOf } from '../references/ast-utils.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Outline view: roadmap → swimlanes → items, with parallel/group nesting and
|
|
35
|
+
* top-level anchors / milestones / footnotes / people / teams / labels /
|
|
36
|
+
* sizes / statuses surfaced as siblings of the swimlanes. Mirrors the layout
|
|
37
|
+
* engine's traversal so what authors see in the outline matches what gets
|
|
38
|
+
* rendered.
|
|
39
|
+
*/
|
|
40
|
+
export class NowlineDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
41
|
+
constructor(_services: NowlineLspServices) {
|
|
42
|
+
/* AST helpers are pure. */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSymbols(
|
|
46
|
+
document: LangiumDocument,
|
|
47
|
+
_params: DocumentSymbolParams,
|
|
48
|
+
_cancelToken?: CancellationToken,
|
|
49
|
+
): MaybePromise<DocumentSymbol[]> {
|
|
50
|
+
const file = fileFromDocument(document);
|
|
51
|
+
if (!file) return [];
|
|
52
|
+
|
|
53
|
+
const symbols: DocumentSymbol[] = [];
|
|
54
|
+
if (file.roadmapDecl) {
|
|
55
|
+
const roadmapSymbol = this.roadmapSymbol(file.roadmapDecl);
|
|
56
|
+
for (const entry of file.roadmapEntries) {
|
|
57
|
+
const child = this.entrySymbol(entry);
|
|
58
|
+
if (child) roadmapSymbol.children!.push(child);
|
|
59
|
+
}
|
|
60
|
+
symbols.push(roadmapSymbol);
|
|
61
|
+
} else {
|
|
62
|
+
for (const entry of file.roadmapEntries) {
|
|
63
|
+
const child = this.entrySymbol(entry);
|
|
64
|
+
if (child) symbols.push(child);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return symbols;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private roadmapSymbol(roadmap: RoadmapDeclaration): DocumentSymbol {
|
|
71
|
+
const range = roadmap.$cstNode!.range;
|
|
72
|
+
const nameRange = nameRangeOf(roadmap) ?? range;
|
|
73
|
+
return {
|
|
74
|
+
name: roadmap.name ?? roadmap.title ?? 'roadmap',
|
|
75
|
+
detail: roadmap.title && roadmap.name ? roadmap.title : 'roadmap',
|
|
76
|
+
kind: SymbolKind.Package,
|
|
77
|
+
range,
|
|
78
|
+
selectionRange: nameRange,
|
|
79
|
+
children: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private entrySymbol(entry: AstNode): DocumentSymbol | undefined {
|
|
84
|
+
if (isSwimlaneDeclaration(entry)) return this.swimlaneSymbol(entry);
|
|
85
|
+
if (isAnchorDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Event);
|
|
86
|
+
if (isMilestoneDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Event);
|
|
87
|
+
if (isFootnoteDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.String);
|
|
88
|
+
if (isPersonDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Constant);
|
|
89
|
+
if (isTeamDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Module);
|
|
90
|
+
if (isLabelDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.EnumMember);
|
|
91
|
+
if (isSizeDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Number);
|
|
92
|
+
if (isStatusDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Enum);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private swimlaneSymbol(lane: SwimlaneDeclaration): DocumentSymbol {
|
|
97
|
+
const sym = this.simpleSymbol(lane, SymbolKind.Namespace, true);
|
|
98
|
+
for (const child of lane.content) {
|
|
99
|
+
const sub = this.trackChildSymbol(child);
|
|
100
|
+
if (sub) sym.children!.push(sub);
|
|
101
|
+
}
|
|
102
|
+
return sym;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private trackChildSymbol(child: SwimlaneContent): DocumentSymbol | undefined {
|
|
106
|
+
if (isItemDeclaration(child)) return this.itemSymbol(child);
|
|
107
|
+
if (isParallelBlock(child)) return this.parallelSymbol(child);
|
|
108
|
+
if (isGroupBlock(child)) return this.groupSymbol(child);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private itemSymbol(item: ItemDeclaration): DocumentSymbol {
|
|
113
|
+
return this.simpleSymbol(item, SymbolKind.Field);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private parallelSymbol(node: ParallelBlock): DocumentSymbol {
|
|
117
|
+
const sym = this.simpleSymbol(node, SymbolKind.Array, true);
|
|
118
|
+
for (const child of node.content) {
|
|
119
|
+
const sub = this.trackChildSymbol(child);
|
|
120
|
+
if (sub) sym.children!.push(sub);
|
|
121
|
+
}
|
|
122
|
+
return sym;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private groupSymbol(node: GroupBlock): DocumentSymbol {
|
|
126
|
+
const sym = this.simpleSymbol(node, SymbolKind.Object, true);
|
|
127
|
+
for (const child of node.content) {
|
|
128
|
+
const sub = this.trackChildSymbol(child);
|
|
129
|
+
if (sub) sym.children!.push(sub);
|
|
130
|
+
}
|
|
131
|
+
return sym;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private simpleSymbol(
|
|
135
|
+
entity: AstNode & { name?: string; title?: string },
|
|
136
|
+
kind: SymbolKind,
|
|
137
|
+
withChildrenSlot = false,
|
|
138
|
+
): DocumentSymbol {
|
|
139
|
+
const range: Range = entity.$cstNode!.range;
|
|
140
|
+
const name = entity.name ?? entity.title ?? entityKind(entity);
|
|
141
|
+
const detail = entity.title && entity.name ? entity.title : entityKind(entity);
|
|
142
|
+
const symbol: DocumentSymbol = {
|
|
143
|
+
name,
|
|
144
|
+
detail,
|
|
145
|
+
kind,
|
|
146
|
+
range,
|
|
147
|
+
selectionRange: nameRangeOf(entity) ?? range,
|
|
148
|
+
};
|
|
149
|
+
if (withChildrenSlot) symbol.children = [];
|
|
150
|
+
return symbol;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { LangiumDocument, MaybePromise } from 'langium';
|
|
2
|
+
import type { HoverProvider } from 'langium/lsp';
|
|
3
|
+
import type { CancellationToken, Hover, HoverParams, Range } from 'vscode-languageserver';
|
|
4
|
+
import { MarkupKind } from 'vscode-languageserver';
|
|
5
|
+
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
6
|
+
import {
|
|
7
|
+
buildEntityIndex,
|
|
8
|
+
declarationAt,
|
|
9
|
+
entityKind,
|
|
10
|
+
fileFromDocument,
|
|
11
|
+
leafAt,
|
|
12
|
+
type NamedEntity,
|
|
13
|
+
propertyValueAt,
|
|
14
|
+
propKey,
|
|
15
|
+
REFERENCE_PROP_KEYS,
|
|
16
|
+
} from '../references/ast-utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hover provider. Surfaces the resolved entity (kind + id + title), plus the
|
|
20
|
+
* subset of properties most authors care about while editing — status, owner,
|
|
21
|
+
* link, date, duration / size — when the cursor sits on either:
|
|
22
|
+
*
|
|
23
|
+
* - The `name=ID` token of an entity declaration.
|
|
24
|
+
* - The value of a reference property (`after:`, `before:`, `owner:`, etc.).
|
|
25
|
+
*/
|
|
26
|
+
export class NowlineHoverProvider implements HoverProvider {
|
|
27
|
+
constructor(_services: NowlineLspServices) {
|
|
28
|
+
/* AST helpers are pure. */
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getHoverContent(
|
|
32
|
+
document: LangiumDocument,
|
|
33
|
+
params: HoverParams,
|
|
34
|
+
_cancelToken?: CancellationToken,
|
|
35
|
+
): MaybePromise<Hover | undefined> {
|
|
36
|
+
const file = fileFromDocument(document);
|
|
37
|
+
if (!file) return undefined;
|
|
38
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
39
|
+
const leaf = leafAt(document, offset);
|
|
40
|
+
if (!leaf) return undefined;
|
|
41
|
+
|
|
42
|
+
const decl = declarationAt(leaf);
|
|
43
|
+
if (decl) return this.hoverFor(decl, leaf.range);
|
|
44
|
+
|
|
45
|
+
const propHit = propertyValueAt(leaf);
|
|
46
|
+
if (propHit && REFERENCE_PROP_KEYS.has(propKey(propHit.prop))) {
|
|
47
|
+
const target = buildEntityIndex(file).get(propHit.value);
|
|
48
|
+
if (!target) return undefined;
|
|
49
|
+
return this.hoverFor(target, leaf.range);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private hoverFor(entity: NamedEntity, range: Range): Hover {
|
|
56
|
+
const kind = entityKind(entity);
|
|
57
|
+
const id = entity.name ?? '';
|
|
58
|
+
const title = entity.title;
|
|
59
|
+
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
const header = id ? `**${kind} \`${id}\`**` : `**${kind}**`;
|
|
62
|
+
lines.push(header);
|
|
63
|
+
if (title) lines.push(`_${title}_`);
|
|
64
|
+
|
|
65
|
+
const props =
|
|
66
|
+
(entity as { properties?: { key: string; value?: string; values?: string[] }[] })
|
|
67
|
+
.properties ?? [];
|
|
68
|
+
const surfaced = [
|
|
69
|
+
'status',
|
|
70
|
+
'owner',
|
|
71
|
+
'link',
|
|
72
|
+
'date',
|
|
73
|
+
'duration',
|
|
74
|
+
'size',
|
|
75
|
+
'effort',
|
|
76
|
+
'capacity',
|
|
77
|
+
];
|
|
78
|
+
const rendered: string[] = [];
|
|
79
|
+
for (const key of surfaced) {
|
|
80
|
+
const prop = props.find((p) => propKey(p as { key: string }) === key);
|
|
81
|
+
if (!prop) continue;
|
|
82
|
+
const value = prop.value ?? prop.values?.join(', ');
|
|
83
|
+
if (value) rendered.push(`- \`${key}:\` ${value}`);
|
|
84
|
+
}
|
|
85
|
+
if (rendered.length) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(...rendered);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
range,
|
|
92
|
+
contents: {
|
|
93
|
+
kind: MarkupKind.Markdown,
|
|
94
|
+
value: lines.join('\n'),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { CstNode, LangiumDocument, MaybePromise } from 'langium';
|
|
2
|
+
import { CstUtils } from 'langium';
|
|
3
|
+
import type { ReferencesProvider } from 'langium/lsp';
|
|
4
|
+
import type { CancellationToken, ReferenceParams } from 'vscode-languageserver';
|
|
5
|
+
import { Location } from 'vscode-languageserver';
|
|
6
|
+
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
7
|
+
import {
|
|
8
|
+
declarationAt,
|
|
9
|
+
fileFromDocument,
|
|
10
|
+
findDeclarationRange,
|
|
11
|
+
leafAt,
|
|
12
|
+
propertyValueAt,
|
|
13
|
+
propKey,
|
|
14
|
+
REFERENCE_PROP_KEYS,
|
|
15
|
+
visitAllProperties,
|
|
16
|
+
} from '../references/ast-utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Custom find-references provider. Resolves the symbol the cursor is on to a
|
|
20
|
+
* target id (either the declared name itself or the value the cursor sits on
|
|
21
|
+
* inside a reference property), then walks the file collecting every
|
|
22
|
+
* text-matching usage in known reference-property positions.
|
|
23
|
+
*
|
|
24
|
+
* Honours `params.context.includeDeclaration`: when true, the declaration
|
|
25
|
+
* site is included in the result list.
|
|
26
|
+
*/
|
|
27
|
+
export class NowlineReferencesProvider implements ReferencesProvider {
|
|
28
|
+
constructor(_services: NowlineLspServices) {
|
|
29
|
+
/* AST helpers are pure. */
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
findReferences(
|
|
33
|
+
document: LangiumDocument,
|
|
34
|
+
params: ReferenceParams,
|
|
35
|
+
_cancelToken?: CancellationToken,
|
|
36
|
+
): MaybePromise<Location[]> {
|
|
37
|
+
const file = fileFromDocument(document);
|
|
38
|
+
if (!file) return [];
|
|
39
|
+
const offset = document.textDocument.offsetAt(params.position);
|
|
40
|
+
const leaf = leafAt(document, offset);
|
|
41
|
+
if (!leaf) return [];
|
|
42
|
+
|
|
43
|
+
const targetId = resolveTargetId(leaf);
|
|
44
|
+
if (!targetId) return [];
|
|
45
|
+
|
|
46
|
+
const locations: Location[] = [];
|
|
47
|
+
const uri = document.uri.toString();
|
|
48
|
+
|
|
49
|
+
if (params.context.includeDeclaration) {
|
|
50
|
+
const declRange = findDeclarationRange(file, targetId);
|
|
51
|
+
if (declRange) locations.push(Location.create(uri, declRange));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
visitAllProperties(file, (prop) => {
|
|
55
|
+
if (!REFERENCE_PROP_KEYS.has(propKey(prop))) return;
|
|
56
|
+
for (const leafNode of valueLeavesOf(prop.$cstNode)) {
|
|
57
|
+
if (leafNode.text === targetId) {
|
|
58
|
+
locations.push(Location.create(uri, leafNode.range));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return locations;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveTargetId(leaf: CstNode): string | undefined {
|
|
68
|
+
const decl = declarationAt(leaf);
|
|
69
|
+
if (decl?.name) return decl.name;
|
|
70
|
+
const propHit = propertyValueAt(leaf);
|
|
71
|
+
if (propHit && REFERENCE_PROP_KEYS.has(propKey(propHit.prop))) {
|
|
72
|
+
return propHit.value;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function* valueLeavesOf(cst: CstNode | undefined): IterableIterator<CstNode> {
|
|
78
|
+
if (!cst) return;
|
|
79
|
+
for (const leaf of CstUtils.flattenCst(cst)) {
|
|
80
|
+
if (leaf.hidden) continue;
|
|
81
|
+
// Skip the property key (`after:` / `owner:` / …) so it doesn't
|
|
82
|
+
// surface as a self-reference.
|
|
83
|
+
if (leaf.text.endsWith(':')) continue;
|
|
84
|
+
yield leaf;
|
|
85
|
+
}
|
|
86
|
+
}
|