@nowline/lsp 0.8.6 → 0.10.0
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/dist/providers/completion.d.ts +7 -1
- package/dist/providers/completion.d.ts.map +1 -1
- package/dist/providers/completion.js +42 -32
- package/dist/providers/completion.js.map +1 -1
- package/dist/providers/definition.d.ts +1 -1
- package/dist/providers/definition.js +1 -1
- package/dist/providers/document-symbols.d.ts +2 -1
- package/dist/providers/document-symbols.d.ts.map +1 -1
- package/dist/providers/document-symbols.js +25 -22
- package/dist/providers/document-symbols.js.map +1 -1
- package/dist/providers/hover.d.ts +9 -0
- package/dist/providers/hover.d.ts.map +1 -1
- package/dist/providers/hover.js +109 -14
- package/dist/providers/hover.js.map +1 -1
- package/dist/providers/references.d.ts.map +1 -1
- package/dist/providers/references.js +3 -1
- package/dist/providers/references.js.map +1 -1
- package/dist/providers/rename.d.ts.map +1 -1
- package/dist/providers/rename.js +2 -2
- package/dist/providers/rename.js.map +1 -1
- package/dist/references/ast-utils.d.ts +41 -31
- package/dist/references/ast-utils.d.ts.map +1 -1
- package/dist/references/ast-utils.js +54 -129
- package/dist/references/ast-utils.js.map +1 -1
- package/package.json +4 -4
- package/src/providers/completion.ts +42 -31
- package/src/providers/definition.ts +1 -1
- package/src/providers/document-symbols.ts +31 -21
- package/src/providers/hover.ts +124 -14
- package/src/providers/references.ts +2 -0
- package/src/providers/rename.ts +2 -1
- package/src/references/ast-utils.ts +81 -127
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
isStatusDeclaration,
|
|
14
14
|
isSwimlaneDeclaration,
|
|
15
15
|
isTeamDeclaration,
|
|
16
|
+
isWaveDeclaration,
|
|
16
17
|
type ParallelBlock,
|
|
17
18
|
type RoadmapDeclaration,
|
|
18
|
-
type SwimlaneContent,
|
|
19
19
|
type SwimlaneDeclaration,
|
|
20
|
+
singleLine,
|
|
20
21
|
} from '@nowline/core';
|
|
22
|
+
import { type TrackNode, trackChildren } from '@nowline/core/internal';
|
|
21
23
|
import type { AstNode, LangiumDocument, MaybePromise } from 'langium';
|
|
22
24
|
import type { DocumentSymbolProvider } from 'langium/lsp';
|
|
23
25
|
import type {
|
|
@@ -30,10 +32,20 @@ import { SymbolKind } from '../lsp-protocol.js';
|
|
|
30
32
|
import type { NowlineLspServices } from '../nowline-lsp-module.js';
|
|
31
33
|
import { entityKind, fileFromDocument, nameRangeOf } from '../references/ast-utils.js';
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* An outline entry is one line of text, but a quoted title can carry
|
|
37
|
+
* explicit line breaks (`"Technology\nSelection"`), so each break is shown
|
|
38
|
+
* as a single space. `undefined` stays `undefined` so callers can fall back
|
|
39
|
+
* (as they must for a title that was only line breaks, which collapses to '').
|
|
40
|
+
*/
|
|
41
|
+
function singleLineTitle(title: string | undefined): string | undefined {
|
|
42
|
+
return title === undefined ? undefined : singleLine(title);
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Outline view: roadmap → swimlanes → items, with parallel/group nesting and
|
|
35
47
|
* top-level anchors / milestones / footnotes / people / teams / labels /
|
|
36
|
-
* sizes / statuses surfaced as siblings of the swimlanes. Mirrors the layout
|
|
48
|
+
* sizes / statuses / waves surfaced as siblings of the swimlanes. Mirrors the layout
|
|
37
49
|
* engine's traversal so what authors see in the outline matches what gets
|
|
38
50
|
* rendered.
|
|
39
51
|
*/
|
|
@@ -71,8 +83,8 @@ export class NowlineDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
|
71
83
|
const range = roadmap.$cstNode!.range;
|
|
72
84
|
const nameRange = nameRangeOf(roadmap) ?? range;
|
|
73
85
|
return {
|
|
74
|
-
name: roadmap.name ?? roadmap.title
|
|
75
|
-
detail: roadmap.title && roadmap.name ? roadmap.title : 'roadmap',
|
|
86
|
+
name: roadmap.name ?? (singleLineTitle(roadmap.title) || 'roadmap'),
|
|
87
|
+
detail: roadmap.title && roadmap.name ? singleLine(roadmap.title) : 'roadmap',
|
|
76
88
|
kind: SymbolKind.Package,
|
|
77
89
|
range,
|
|
78
90
|
selectionRange: nameRange,
|
|
@@ -90,19 +102,15 @@ export class NowlineDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
|
90
102
|
if (isLabelDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.EnumMember);
|
|
91
103
|
if (isSizeDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Number);
|
|
92
104
|
if (isStatusDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Enum);
|
|
105
|
+
if (isWaveDeclaration(entry)) return this.simpleSymbol(entry, SymbolKind.Struct);
|
|
93
106
|
return undefined;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
private swimlaneSymbol(lane: SwimlaneDeclaration): DocumentSymbol {
|
|
97
|
-
|
|
98
|
-
for (const child of lane.content) {
|
|
99
|
-
const sub = this.trackChildSymbol(child);
|
|
100
|
-
if (sub) sym.children!.push(sub);
|
|
101
|
-
}
|
|
102
|
-
return sym;
|
|
110
|
+
return this.withTrackChildren(this.simpleSymbol(lane, SymbolKind.Namespace, true), lane);
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
private trackChildSymbol(child:
|
|
113
|
+
private trackChildSymbol(child: TrackNode): DocumentSymbol | undefined {
|
|
106
114
|
if (isItemDeclaration(child)) return this.itemSymbol(child);
|
|
107
115
|
if (isParallelBlock(child)) return this.parallelSymbol(child);
|
|
108
116
|
if (isGroupBlock(child)) return this.groupSymbol(child);
|
|
@@ -114,17 +122,19 @@ export class NowlineDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
private parallelSymbol(node: ParallelBlock): DocumentSymbol {
|
|
117
|
-
|
|
118
|
-
for (const child of node.content) {
|
|
119
|
-
const sub = this.trackChildSymbol(child);
|
|
120
|
-
if (sub) sym.children!.push(sub);
|
|
121
|
-
}
|
|
122
|
-
return sym;
|
|
125
|
+
return this.withTrackChildren(this.simpleSymbol(node, SymbolKind.Array, true), node);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
private groupSymbol(node: GroupBlock): DocumentSymbol {
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
return this.withTrackChildren(this.simpleSymbol(node, SymbolKind.Object, true), node);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The container's track children (descriptions skipped) as nested symbols.
|
|
133
|
+
private withTrackChildren(
|
|
134
|
+
sym: DocumentSymbol,
|
|
135
|
+
container: SwimlaneDeclaration | GroupBlock | ParallelBlock,
|
|
136
|
+
): DocumentSymbol {
|
|
137
|
+
for (const child of trackChildren(container)) {
|
|
128
138
|
const sub = this.trackChildSymbol(child);
|
|
129
139
|
if (sub) sym.children!.push(sub);
|
|
130
140
|
}
|
|
@@ -137,8 +147,8 @@ export class NowlineDocumentSymbolProvider implements DocumentSymbolProvider {
|
|
|
137
147
|
withChildrenSlot = false,
|
|
138
148
|
): DocumentSymbol {
|
|
139
149
|
const range: Range = entity.$cstNode!.range;
|
|
140
|
-
const name = entity.name ?? entity.title
|
|
141
|
-
const detail = entity.title && entity.name ? entity.title : entityKind(entity);
|
|
150
|
+
const name = entity.name ?? (singleLineTitle(entity.title) || entityKind(entity));
|
|
151
|
+
const detail = entity.title && entity.name ? singleLine(entity.title) : entityKind(entity);
|
|
142
152
|
const symbol: DocumentSymbol = {
|
|
143
153
|
name,
|
|
144
154
|
detail,
|
package/src/providers/hover.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
assignWaves,
|
|
3
|
+
isGroupBlock,
|
|
4
|
+
isItemDeclaration,
|
|
5
|
+
isNonWorkingDeclaration,
|
|
6
|
+
isParallelBlock,
|
|
7
|
+
isSwimlaneDeclaration,
|
|
8
|
+
isWaveDeclaration,
|
|
9
|
+
isWorkingDeclaration,
|
|
10
|
+
type NowlineFile,
|
|
11
|
+
ownWaves,
|
|
12
|
+
singleLine,
|
|
13
|
+
type WaveAssignment,
|
|
14
|
+
type WaveDeclaration,
|
|
15
|
+
waveIndexMap,
|
|
16
|
+
} from '@nowline/core';
|
|
17
|
+
import { classifyCalendarDeclaration } from '@nowline/core/internal';
|
|
18
|
+
import type { AstNode, LangiumDocument, MaybePromise } from 'langium';
|
|
2
19
|
import type { HoverProvider } from 'langium/lsp';
|
|
3
20
|
import type { CancellationToken, Hover, HoverParams, Range } from '../lsp-protocol.js';
|
|
4
21
|
import { MarkupKind } from '../lsp-protocol.js';
|
|
@@ -8,6 +25,7 @@ import {
|
|
|
8
25
|
declarationAt,
|
|
9
26
|
entityKind,
|
|
10
27
|
fileFromDocument,
|
|
28
|
+
isCalendarDeclaration,
|
|
11
29
|
leafAt,
|
|
12
30
|
type NamedEntity,
|
|
13
31
|
propertyValueAt,
|
|
@@ -22,6 +40,15 @@ import {
|
|
|
22
40
|
*
|
|
23
41
|
* - The `name=ID` token of an entity declaration.
|
|
24
42
|
* - The value of a reference property (`after:`, `before:`, `owner:`, etc.).
|
|
43
|
+
*
|
|
44
|
+
* Waves (specs/waves.md): a wave shows its position (`wave k of n`), title,
|
|
45
|
+
* description and member count; an item in a roadmap with waves shows its
|
|
46
|
+
* effective wave, or `background (no wave)`. Waves come from the file's own
|
|
47
|
+
* declarations only — the LSP does not resolve includes.
|
|
48
|
+
*
|
|
49
|
+
* Calendar declarations (`non-working`, `working`, specs/working-calendar.md):
|
|
50
|
+
* the declaration shows its id, title, and `date`, `every`, `start` and `end`,
|
|
51
|
+
* plus a note when its title is never drawn on the chart (§7.4).
|
|
25
52
|
*/
|
|
26
53
|
export class NowlineHoverProvider implements HoverProvider {
|
|
27
54
|
constructor(_services: NowlineLspServices) {
|
|
@@ -40,19 +67,19 @@ export class NowlineHoverProvider implements HoverProvider {
|
|
|
40
67
|
if (!leaf) return undefined;
|
|
41
68
|
|
|
42
69
|
const decl = declarationAt(leaf);
|
|
43
|
-
if (decl) return this.hoverFor(decl, leaf.range);
|
|
70
|
+
if (decl) return this.hoverFor(file, decl, leaf.range);
|
|
44
71
|
|
|
45
72
|
const propHit = propertyValueAt(leaf);
|
|
46
73
|
if (propHit && REFERENCE_PROP_KEYS.has(propKey(propHit.prop))) {
|
|
47
74
|
const target = buildEntityIndex(file).get(propHit.value);
|
|
48
75
|
if (!target) return undefined;
|
|
49
|
-
return this.hoverFor(target, leaf.range);
|
|
76
|
+
return this.hoverFor(file, target, leaf.range);
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
return undefined;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
private hoverFor(entity: NamedEntity, range: Range): Hover {
|
|
82
|
+
private hoverFor(file: NowlineFile, entity: NamedEntity, range: Range): Hover {
|
|
56
83
|
const kind = entityKind(entity);
|
|
57
84
|
const id = entity.name ?? '';
|
|
58
85
|
const title = entity.title;
|
|
@@ -61,20 +88,14 @@ export class NowlineHoverProvider implements HoverProvider {
|
|
|
61
88
|
const header = id ? `**${kind} \`${id}\`**` : `**${kind}**`;
|
|
62
89
|
lines.push(header);
|
|
63
90
|
if (title) lines.push(`_${title}_`);
|
|
91
|
+
// Own paragraph so Markdown does not fold the wave lines into the title.
|
|
92
|
+
const waveInfo = waveLines(file, entity);
|
|
93
|
+
if (waveInfo.length) lines.push('', ...waveInfo);
|
|
64
94
|
|
|
65
95
|
const props =
|
|
66
96
|
(entity as { properties?: { key: string; value?: string; values?: string[] }[] })
|
|
67
97
|
.properties ?? [];
|
|
68
|
-
const surfaced =
|
|
69
|
-
'status',
|
|
70
|
-
'owner',
|
|
71
|
-
'link',
|
|
72
|
-
'date',
|
|
73
|
-
'duration',
|
|
74
|
-
'size',
|
|
75
|
-
'effort',
|
|
76
|
-
'capacity',
|
|
77
|
-
];
|
|
98
|
+
const surfaced = isCalendarDeclaration(entity) ? CALENDAR_SURFACED : ITEM_SURFACED;
|
|
78
99
|
const rendered: string[] = [];
|
|
79
100
|
for (const key of surfaced) {
|
|
80
101
|
const prop = props.find((p) => propKey(p as { key: string }) === key);
|
|
@@ -86,6 +107,8 @@ export class NowlineHoverProvider implements HoverProvider {
|
|
|
86
107
|
lines.push('');
|
|
87
108
|
lines.push(...rendered);
|
|
88
109
|
}
|
|
110
|
+
const note = undrawnTitleNote(entity);
|
|
111
|
+
if (note) lines.push('', `_${note}_`);
|
|
89
112
|
|
|
90
113
|
return {
|
|
91
114
|
range,
|
|
@@ -96,3 +119,90 @@ export class NowlineHoverProvider implements HoverProvider {
|
|
|
96
119
|
};
|
|
97
120
|
}
|
|
98
121
|
}
|
|
122
|
+
|
|
123
|
+
const ITEM_SURFACED = ['status', 'owner', 'link', 'date', 'duration', 'size', 'effort', 'capacity'];
|
|
124
|
+
|
|
125
|
+
// A `non-working` / `working` declaration is described by its days, not by an
|
|
126
|
+
// item's status or duration (specs/working-calendar.md §4.1).
|
|
127
|
+
const CALENDAR_SURFACED = ['date', 'every', 'start', 'end'];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Why a calendar declaration's title is never drawn, or undefined when it is
|
|
131
|
+
* drawn, there is no title, or the shape is malformed (NW1, §4.1; core's
|
|
132
|
+
* `classifyCalendarDeclaration` reads the shape). Dated,
|
|
133
|
+
* titled declarations are labelled, `non-working` or `working`; a titled
|
|
134
|
+
* bounded recurrence is not (specs/working-calendar.md §7.4 "Which rules are
|
|
135
|
+
* labelled"). The open-ended week is the baseline and gets no note.
|
|
136
|
+
*/
|
|
137
|
+
function undrawnTitleNote(entity: NamedEntity): string | undefined {
|
|
138
|
+
if (!isNonWorkingDeclaration(entity) && !isWorkingDeclaration(entity)) return undefined;
|
|
139
|
+
if (!entity.title) return undefined;
|
|
140
|
+
const { shape } = classifyCalendarDeclaration(entity);
|
|
141
|
+
if (shape === 'half-bounded-recurrence' || shape === 'bounded-recurrence') {
|
|
142
|
+
return 'Title not drawn on the chart: bounded recurrences are not labelled.';
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wave lines for the hover: the wave summary on a wave declaration, the
|
|
149
|
+
* effective wave on an item. Empty when the file declares no waves.
|
|
150
|
+
*/
|
|
151
|
+
function waveLines(file: NowlineFile, entity: NamedEntity): string[] {
|
|
152
|
+
if (!isWaveDeclaration(entity) && !isItemDeclaration(entity)) return [];
|
|
153
|
+
const waves = ownWaves(file);
|
|
154
|
+
if (waves.length === 0) return [];
|
|
155
|
+
const waveIds = waveIndexMap(file);
|
|
156
|
+
const lanes = file.roadmapEntries.filter(isSwimlaneDeclaration);
|
|
157
|
+
const assignment = assignWaves(lanes, waveIds);
|
|
158
|
+
if (isWaveDeclaration(entity)) return waveSummary(entity, waves, waveIds, assignment);
|
|
159
|
+
const ew = assignment.waveOf.get(entity);
|
|
160
|
+
if (ew === undefined) return ['background (no wave)'];
|
|
161
|
+
const wave = waves[ew - 1];
|
|
162
|
+
const source = waveSource(entity, assignment);
|
|
163
|
+
if (source === entity) return [`wave "${wave.name}"`];
|
|
164
|
+
return [`wave "${wave.name}" (inherited from ${describeContainer(source)})`];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function waveSummary(
|
|
168
|
+
wave: WaveDeclaration,
|
|
169
|
+
waves: readonly WaveDeclaration[],
|
|
170
|
+
waveIds: ReadonlyMap<string, number>,
|
|
171
|
+
assignment: WaveAssignment,
|
|
172
|
+
): string[] {
|
|
173
|
+
const k = wave.name ? waveIds.get(wave.name) : undefined;
|
|
174
|
+
// A repeated wave id (NL.E0300) only counts its first declaration.
|
|
175
|
+
if (k === undefined || waves[k - 1] !== wave) return [];
|
|
176
|
+
const lines = [`wave ${k} of ${waves.length}`];
|
|
177
|
+
if (wave.description?.text) lines.push('', wave.description.text);
|
|
178
|
+
let members = 0;
|
|
179
|
+
for (const [node, ew] of assignment.waveOf) {
|
|
180
|
+
if (ew === k && isItemDeclaration(node)) members++;
|
|
181
|
+
}
|
|
182
|
+
lines.push('', `${members} ${members === 1 ? 'member' : 'members'}`);
|
|
183
|
+
const labels = wave.properties.find((p) => propKey(p) === 'labels');
|
|
184
|
+
const labelIds = labels?.values ?? (labels?.value ? [labels.value] : []);
|
|
185
|
+
if (labelIds.length) lines.push('', `labels: ${labelIds.join(', ')}`);
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The node that sets an item's effective wave: the outermost of the item and
|
|
190
|
+
// its enclosing groups / parallels that has one. An enclosing container's wave
|
|
191
|
+
// wins over the item's own (specs/waves.md §4.4), and it reaches the item only
|
|
192
|
+
// through the containers in between.
|
|
193
|
+
function waveSource(item: AstNode, assignment: WaveAssignment): AstNode {
|
|
194
|
+
let source = item;
|
|
195
|
+
let node = item.$container;
|
|
196
|
+
while ((isGroupBlock(node) || isParallelBlock(node)) && assignment.waveOf.has(node)) {
|
|
197
|
+
source = node;
|
|
198
|
+
node = node.$container;
|
|
199
|
+
}
|
|
200
|
+
return source;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function describeContainer(node: AstNode): string {
|
|
204
|
+
const kind = entityKind(node);
|
|
205
|
+
const named = node as { name?: string; title?: string };
|
|
206
|
+
const label = named.name ?? (named.title === undefined ? undefined : singleLine(named.title));
|
|
207
|
+
return label ? `${kind} "${label}"` : kind;
|
|
208
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
declarationAt,
|
|
9
9
|
fileFromDocument,
|
|
10
10
|
findDeclarationRange,
|
|
11
|
+
isCalendarDeclaration,
|
|
11
12
|
leafAt,
|
|
12
13
|
propertyValueAt,
|
|
13
14
|
propKey,
|
|
@@ -66,6 +67,7 @@ export class NowlineReferencesProvider implements ReferencesProvider {
|
|
|
66
67
|
|
|
67
68
|
function resolveTargetId(leaf: CstNode): string | undefined {
|
|
68
69
|
const decl = declarationAt(leaf);
|
|
70
|
+
if (decl && isCalendarDeclaration(decl)) return undefined;
|
|
69
71
|
if (decl?.name) return decl.name;
|
|
70
72
|
const propHit = propertyValueAt(leaf);
|
|
71
73
|
if (propHit && REFERENCE_PROP_KEYS.has(propKey(propHit.prop))) {
|
package/src/providers/rename.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
declarationAt,
|
|
17
17
|
fileFromDocument,
|
|
18
18
|
findDeclarationRange,
|
|
19
|
+
isCalendarDeclaration,
|
|
19
20
|
leafAt,
|
|
20
21
|
propertyValueAt,
|
|
21
22
|
propKey,
|
|
@@ -93,7 +94,7 @@ function renameRangeAt(document: LangiumDocument, position: Position): RenameTar
|
|
|
93
94
|
const leaf = leafAt(document, offset);
|
|
94
95
|
if (!leaf) return undefined;
|
|
95
96
|
const decl = declarationAt(leaf);
|
|
96
|
-
if (decl
|
|
97
|
+
if (decl && !isCalendarDeclaration(decl) && decl.name === leaf.text) {
|
|
97
98
|
return { id: decl.name, range: leaf.range };
|
|
98
99
|
}
|
|
99
100
|
const hit = propertyValueAt(leaf);
|
|
@@ -3,21 +3,21 @@ import type {
|
|
|
3
3
|
EntityProperty,
|
|
4
4
|
FootnoteDeclaration,
|
|
5
5
|
GroupBlock,
|
|
6
|
-
GroupContent,
|
|
7
6
|
ItemDeclaration,
|
|
8
7
|
LabelDeclaration,
|
|
9
8
|
MilestoneDeclaration,
|
|
9
|
+
NonWorkingDeclaration,
|
|
10
10
|
NowlineFile,
|
|
11
11
|
ParallelBlock,
|
|
12
|
-
ParallelContent,
|
|
13
12
|
PersonDeclaration,
|
|
14
13
|
SizeDeclaration,
|
|
15
14
|
StatusDeclaration,
|
|
16
15
|
StyleDeclaration,
|
|
17
|
-
SwimlaneContent,
|
|
18
16
|
SwimlaneDeclaration,
|
|
19
17
|
SymbolDeclaration,
|
|
20
18
|
TeamDeclaration,
|
|
19
|
+
WaveDeclaration,
|
|
20
|
+
WorkingDeclaration,
|
|
21
21
|
} from '@nowline/core';
|
|
22
22
|
import {
|
|
23
23
|
isAnchorDeclaration,
|
|
@@ -26,19 +26,22 @@ import {
|
|
|
26
26
|
isItemDeclaration,
|
|
27
27
|
isLabelDeclaration,
|
|
28
28
|
isMilestoneDeclaration,
|
|
29
|
+
isNonWorkingDeclaration,
|
|
29
30
|
isNowlineFile,
|
|
30
31
|
isParallelBlock,
|
|
31
32
|
isPersonDeclaration,
|
|
32
|
-
isPersonMemberRef,
|
|
33
33
|
isSizeDeclaration,
|
|
34
34
|
isStatusDeclaration,
|
|
35
35
|
isStyleDeclaration,
|
|
36
36
|
isSwimlaneDeclaration,
|
|
37
37
|
isSymbolDeclaration,
|
|
38
38
|
isTeamDeclaration,
|
|
39
|
+
isWaveDeclaration,
|
|
40
|
+
isWorkingDeclaration,
|
|
39
41
|
} from '@nowline/core';
|
|
42
|
+
import { declarations, propKey, visitProperties } from '@nowline/core/internal';
|
|
40
43
|
import type { AstNode, CstNode, LangiumDocument } from 'langium';
|
|
41
|
-
import { CstUtils } from 'langium';
|
|
44
|
+
import { CstUtils, GrammarUtils } from 'langium';
|
|
42
45
|
import type { Range } from '../lsp-protocol.js';
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -49,7 +52,7 @@ import type { Range } from '../lsp-protocol.js';
|
|
|
49
52
|
* Wider than the validator's `checkReferenceResolution` (which is strict only
|
|
50
53
|
* for sequencing — `after` / `before` / `on`). Categorisation references
|
|
51
54
|
* (`size` / `status` / `style` / `labels`), decoration references
|
|
52
|
-
* (`icon` / `capacity-icon`), and ownership
|
|
55
|
+
* (`icon` / `capacity-icon`), and the ownership reference (`owner`) are
|
|
53
56
|
* permissive in the validator (an undeclared id is not an error) but still
|
|
54
57
|
* navigable when a declaration exists.
|
|
55
58
|
*
|
|
@@ -57,60 +60,44 @@ import type { Range } from '../lsp-protocol.js';
|
|
|
57
60
|
* form was removed in favor of the spec-mandated reverse direction.
|
|
58
61
|
* Footnote association flows only from the footnote declaration's `on:`,
|
|
59
62
|
* and the validator now rejects `footnote:` as a property on host entities.
|
|
63
|
+
*
|
|
64
|
+
* Every key here must be one the validator accepts; `reference-keys.test.ts`
|
|
65
|
+
* checks that.
|
|
60
66
|
*/
|
|
61
67
|
export const REFERENCE_PROP_KEYS = new Set([
|
|
62
68
|
'after',
|
|
63
69
|
'before',
|
|
64
|
-
'depends',
|
|
65
70
|
'on',
|
|
66
71
|
'owner',
|
|
67
|
-
'team',
|
|
68
72
|
'size',
|
|
69
73
|
'status',
|
|
70
74
|
'style',
|
|
71
75
|
'labels',
|
|
72
76
|
'icon',
|
|
73
77
|
'capacity-icon',
|
|
78
|
+
'wave',
|
|
74
79
|
]);
|
|
75
80
|
|
|
76
|
-
/** Status value-completion source: built-in statuses ship with the renderer. */
|
|
77
|
-
export const BUILTIN_STATUSES: readonly string[] = [
|
|
78
|
-
'planned',
|
|
79
|
-
'in-progress',
|
|
80
|
-
'done',
|
|
81
|
-
'at-risk',
|
|
82
|
-
'blocked',
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Built-in `capacity-icon:` vocabulary. Mirrors `BUILTIN_CAPACITY_ICONS` in
|
|
87
|
-
* the validator — kept as a literal here so the LSP doesn't have to import a
|
|
88
|
-
* non-public symbol from `@nowline/core`.
|
|
89
|
-
*/
|
|
90
|
-
export const BUILTIN_CAPACITY_ICONS: readonly string[] = [
|
|
91
|
-
'none',
|
|
92
|
-
'multiplier',
|
|
93
|
-
'person',
|
|
94
|
-
'people',
|
|
95
|
-
'points',
|
|
96
|
-
'time',
|
|
97
|
-
];
|
|
98
|
-
|
|
99
81
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
82
|
+
* Value-completion sources. The vocabularies are core's (`value-tables.ts`):
|
|
83
|
+
* the validator checks values against the same lists, so the editor offers
|
|
84
|
+
* exactly what validates. Kept as `@nowline/lsp` exports. `WEEKDAY_NAMES` is
|
|
85
|
+
* the weekday names `every:` takes, Sunday first.
|
|
102
86
|
*/
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
87
|
+
export {
|
|
88
|
+
BUILTIN_CAPACITY_ICONS,
|
|
89
|
+
BUILTIN_ICON_NAMES,
|
|
90
|
+
BUILTIN_STATUSES,
|
|
91
|
+
CALENDAR_MODES,
|
|
92
|
+
COLOR_NAMES,
|
|
93
|
+
INCLUDE_MODES,
|
|
94
|
+
NON_WORKING_DISPLAYS,
|
|
95
|
+
STYLE_PROP_ENUMS,
|
|
96
|
+
WEEKDAYS as WEEKDAY_NAMES,
|
|
97
|
+
} from '@nowline/core/internal';
|
|
109
98
|
|
|
110
|
-
/**
|
|
111
|
-
export
|
|
112
|
-
return prop.key.endsWith(':') ? prop.key.slice(0, -1) : prop.key;
|
|
113
|
-
}
|
|
99
|
+
/** `prop.key` without a trailing `:` (core's, kept as an `@nowline/lsp` export). */
|
|
100
|
+
export { propKey };
|
|
114
101
|
|
|
115
102
|
/**
|
|
116
103
|
* Named declaration shapes we expose for navigation. All have `name?: string`,
|
|
@@ -119,6 +106,11 @@ export function propKey(prop: { key: string }): string {
|
|
|
119
106
|
* Includes both roadmap-section declarations (item, swimlane, person, …) and
|
|
120
107
|
* config-section declarations (`style`, `symbol`) so cmd+click on
|
|
121
108
|
* `style:enterprise-style` and `icon:my-symbol` resolves to the right place.
|
|
109
|
+
*
|
|
110
|
+
* `non-working` and `working` declarations are here so `declarationAt` (hover)
|
|
111
|
+
* sees them, but `collectNamedEntities` skips them: nothing references a
|
|
112
|
+
* calendar declaration by id, so they stay out of reference completion,
|
|
113
|
+
* definition and rename.
|
|
122
114
|
*/
|
|
123
115
|
export type NamedEntity =
|
|
124
116
|
| RoadmapEntryNamed
|
|
@@ -126,7 +118,9 @@ export type NamedEntity =
|
|
|
126
118
|
| ParallelBlock
|
|
127
119
|
| GroupBlock
|
|
128
120
|
| StyleDeclaration
|
|
129
|
-
| SymbolDeclaration
|
|
121
|
+
| SymbolDeclaration
|
|
122
|
+
| NonWorkingDeclaration
|
|
123
|
+
| WorkingDeclaration;
|
|
130
124
|
|
|
131
125
|
type RoadmapEntryNamed =
|
|
132
126
|
| SwimlaneDeclaration
|
|
@@ -137,64 +131,33 @@ type RoadmapEntryNamed =
|
|
|
137
131
|
| TeamDeclaration
|
|
138
132
|
| LabelDeclaration
|
|
139
133
|
| SizeDeclaration
|
|
140
|
-
| StatusDeclaration
|
|
134
|
+
| StatusDeclaration
|
|
135
|
+
| WaveDeclaration;
|
|
141
136
|
|
|
142
137
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
* True for a `non-working` or `working` declaration. Hover describes them, but
|
|
139
|
+
* nothing references a calendar declaration by id, so definition, references
|
|
140
|
+
* and rename leave them alone.
|
|
141
|
+
*/
|
|
142
|
+
export function isCalendarDeclaration(entity: AstNode): boolean {
|
|
143
|
+
return isNonWorkingDeclaration(entity) || isWorkingDeclaration(entity);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collect every entity that owns an `id`, in the order of core's declaration
|
|
148
|
+
* walk (config entries first, then roadmap entries with their track content or
|
|
149
|
+
* nested teams): the roadmap-section entities `@nowline/core`'s
|
|
150
|
+
* `referenceableIds` covers, plus the named config-section declarations
|
|
151
|
+
* (`style`, `symbol`) so style and symbol references navigate correctly.
|
|
152
|
+
* The LSP skips the roadmap line itself (not walked here) and the calendar
|
|
153
|
+
* declarations (`isCalendarDeclaration`); anonymous declarations have no id.
|
|
148
154
|
*/
|
|
149
155
|
export function collectNamedEntities(file: NowlineFile): NamedEntity[] {
|
|
150
156
|
const out: NamedEntity[] = [];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
else if (isSymbolDeclaration(entry) && entry.name) out.push(entry);
|
|
157
|
+
for (const node of declarations(file, { config: true })) {
|
|
158
|
+
if (!(node as { name?: string }).name || isCalendarDeclaration(node)) continue;
|
|
159
|
+
out.push(node as NamedEntity);
|
|
155
160
|
}
|
|
156
|
-
|
|
157
|
-
const visitTeam = (team: TeamDeclaration) => {
|
|
158
|
-
if (team.name) out.push(team);
|
|
159
|
-
for (const c of team.content) {
|
|
160
|
-
if (isTeamDeclaration(c)) visitTeam(c);
|
|
161
|
-
// `PersonMemberRef` and `DescriptionDirective` are references / prose,
|
|
162
|
-
// not declarations — skip.
|
|
163
|
-
else if (isPersonMemberRef(c)) {
|
|
164
|
-
/* skip */
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const visitTrackChild = (child: SwimlaneContent | GroupContent | ParallelContent) => {
|
|
170
|
-
if (isItemDeclaration(child) && child.name) out.push(child);
|
|
171
|
-
else if (isParallelBlock(child)) {
|
|
172
|
-
if (child.name) out.push(child);
|
|
173
|
-
for (const sub of child.content) visitTrackChild(sub);
|
|
174
|
-
} else if (isGroupBlock(child)) {
|
|
175
|
-
if (child.name) out.push(child);
|
|
176
|
-
for (const sub of child.content) visitTrackChild(sub);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
for (const entry of file.roadmapEntries) {
|
|
181
|
-
if (isSwimlaneDeclaration(entry)) {
|
|
182
|
-
if (entry.name) out.push(entry);
|
|
183
|
-
for (const c of entry.content) visitTrackChild(c);
|
|
184
|
-
} else if (isTeamDeclaration(entry)) visitTeam(entry);
|
|
185
|
-
else if (
|
|
186
|
-
isPersonDeclaration(entry) ||
|
|
187
|
-
isAnchorDeclaration(entry) ||
|
|
188
|
-
isMilestoneDeclaration(entry) ||
|
|
189
|
-
isFootnoteDeclaration(entry) ||
|
|
190
|
-
isLabelDeclaration(entry) ||
|
|
191
|
-
isSizeDeclaration(entry) ||
|
|
192
|
-
isStatusDeclaration(entry)
|
|
193
|
-
) {
|
|
194
|
-
if (entry.name) out.push(entry);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
161
|
return out;
|
|
199
162
|
}
|
|
200
163
|
|
|
@@ -213,27 +176,7 @@ export function buildEntityIndex(file: NowlineFile): Map<string, NamedEntity> {
|
|
|
213
176
|
}
|
|
214
177
|
|
|
215
178
|
/** Visit every `EntityProperty` reachable from a roadmap entry, depth-first. */
|
|
216
|
-
export
|
|
217
|
-
node: AstNode,
|
|
218
|
-
visit: (prop: EntityProperty, owner: AstNode) => void,
|
|
219
|
-
): void {
|
|
220
|
-
const walk = (n: AstNode): void => {
|
|
221
|
-
const props = (n as { properties?: EntityProperty[] }).properties;
|
|
222
|
-
if (Array.isArray(props)) {
|
|
223
|
-
for (const p of props) visit(p, n);
|
|
224
|
-
}
|
|
225
|
-
if (isSwimlaneDeclaration(n)) {
|
|
226
|
-
for (const c of n.content) walk(c);
|
|
227
|
-
} else if (isParallelBlock(n) || isGroupBlock(n)) {
|
|
228
|
-
for (const c of n.content) walk(c);
|
|
229
|
-
} else if (isTeamDeclaration(n)) {
|
|
230
|
-
for (const c of n.content) {
|
|
231
|
-
if (isTeamDeclaration(c) || isPersonDeclaration(c)) walk(c);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
walk(node);
|
|
236
|
-
}
|
|
179
|
+
export { visitProperties };
|
|
237
180
|
|
|
238
181
|
/**
|
|
239
182
|
* Iterate every `EntityProperty` in the file, including those on the roadmap
|
|
@@ -297,13 +240,16 @@ export function propertyValueAt(
|
|
|
297
240
|
|
|
298
241
|
/**
|
|
299
242
|
* When `leaf` is the `name=ID` token of a named entity declaration, return the
|
|
300
|
-
* declaring entity. Returns `undefined` for any other position
|
|
243
|
+
* declaring entity. Returns `undefined` for any other position, including the
|
|
244
|
+
* declaration keyword (`wave wave "W"` names its wave after the keyword).
|
|
301
245
|
*/
|
|
302
246
|
export function declarationAt(leaf: CstNode | undefined): NamedEntity | undefined {
|
|
303
247
|
if (!leaf) return undefined;
|
|
304
248
|
const owner = leaf.astNode as AstNode & { name?: string };
|
|
305
249
|
if (!owner || typeof owner.name !== 'string') return undefined;
|
|
306
250
|
if (owner.name !== leaf.text) return undefined;
|
|
251
|
+
const nameNode = nameNodeOf(owner);
|
|
252
|
+
if (!nameNode || leaf.offset < nameNode.offset || leaf.end > nameNode.end) return undefined;
|
|
307
253
|
if (
|
|
308
254
|
isSwimlaneDeclaration(owner) ||
|
|
309
255
|
isItemDeclaration(owner) ||
|
|
@@ -317,8 +263,11 @@ export function declarationAt(leaf: CstNode | undefined): NamedEntity | undefine
|
|
|
317
263
|
isLabelDeclaration(owner) ||
|
|
318
264
|
isSizeDeclaration(owner) ||
|
|
319
265
|
isStatusDeclaration(owner) ||
|
|
266
|
+
isWaveDeclaration(owner) ||
|
|
320
267
|
isStyleDeclaration(owner) ||
|
|
321
|
-
isSymbolDeclaration(owner)
|
|
268
|
+
isSymbolDeclaration(owner) ||
|
|
269
|
+
isNonWorkingDeclaration(owner) ||
|
|
270
|
+
isWorkingDeclaration(owner)
|
|
322
271
|
) {
|
|
323
272
|
return owner as NamedEntity;
|
|
324
273
|
}
|
|
@@ -348,6 +297,8 @@ export function findDeclarationRange(file: NowlineFile, id: string): Range | und
|
|
|
348
297
|
* Render a one-line "type id" label used by hover and document-symbols.
|
|
349
298
|
*/
|
|
350
299
|
export function entityKind(entity: AstNode): string {
|
|
300
|
+
// `NonWorkingDeclaration` would lowercase to `nonworking`; the keyword is hyphenated.
|
|
301
|
+
if (entity.$type === 'NonWorkingDeclaration') return 'non-working';
|
|
351
302
|
return entity.$type
|
|
352
303
|
.replace(/Declaration$/, '')
|
|
353
304
|
.replace(/Block$/, '')
|
|
@@ -355,18 +306,21 @@ export function entityKind(entity: AstNode): string {
|
|
|
355
306
|
}
|
|
356
307
|
|
|
357
308
|
/**
|
|
358
|
-
* Find the
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
309
|
+
* Find the range of the given entity's `name=ID` token. Uses the grammar
|
|
310
|
+
* assignment rather than matching leaf text, so a name that equals the
|
|
311
|
+
* declaration keyword (`wave wave "W"`) resolves to the name, not the
|
|
312
|
+
* keyword. Returns `undefined` when the entity has no `name` (anonymous
|
|
313
|
+
* declaration) or no CST node.
|
|
362
314
|
*/
|
|
363
315
|
export function nameRangeOf(entity: AstNode): Range | undefined {
|
|
316
|
+
return nameNodeOf(entity)?.range;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The CST node assigned to the entity's `name` property.
|
|
320
|
+
function nameNodeOf(entity: AstNode): CstNode | undefined {
|
|
364
321
|
const name = (entity as { name?: string }).name;
|
|
365
322
|
if (!name || !entity.$cstNode) return undefined;
|
|
366
|
-
|
|
367
|
-
if (leaf.text === name) return leaf.range;
|
|
368
|
-
}
|
|
369
|
-
return undefined;
|
|
323
|
+
return GrammarUtils.findNodeForProperty(entity.$cstNode, 'name');
|
|
370
324
|
}
|
|
371
325
|
|
|
372
326
|
/**
|