Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/language/src/reqlan-reference-code-lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isFileReference,
isFileSymbolReference,
isIdea,
isIdeaDeclaration,
isIdeaSet,
isLocalReference,
isOneLinerIdea,
Expand Down Expand Up @@ -65,8 +66,10 @@ export function classifyReferenceForCodeLens(
);
return classifyResolvedFileLink(link);
}
// Attribute targets are not idea declarations.
// rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing]
const ideaTarget = peekResolvedRef(referenceIdea(target));
if (ideaTarget) {
if (ideaTarget && isIdeaDeclaration(ideaTarget)) {
return { kind: 'idea', declaration: ideaTarget };
}
if (isQualifiedReference(target)) {
Expand Down
6 changes: 4 additions & 2 deletions packages/language/src/reqlan-references.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Shared helpers for the reference target union types.
* rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing]
*/
import { AstUtils, type Reference } from 'langium';
import {
Expand All @@ -8,8 +9,8 @@ import {
isLocalReference,
isModel,
isQualifiedReference,
type IdeaDeclaration,
type Import,
type QualifiedIdeaTarget,
type QualifiedReference,
type ReferenceTarget
} from './generated/ast.js';
Expand All @@ -26,7 +27,8 @@ export function referenceImport(target: ReferenceTarget): Reference<Import> | un
return undefined;
}

export function referenceIdea(target: ReferenceTarget): Reference<IdeaDeclaration> | undefined {
/** Qualified targets include attributes. Local targets are idea declarations. */
export function referenceIdea(target: ReferenceTarget): Reference<QualifiedIdeaTarget> | undefined {
if (isQualifiedReference(target) || isLocalReference(target)) {
return target.idea;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/language/src/reqlan.langium
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,14 @@ FileSymbolReference:
WildcardReference:
pathPattern=STRING '.' ideaPattern=WILDCARD_NAME;

// The second name of a qualified reference is an idea, an ideaset member, or an attribute.
// rq:["../../../reqlan rq/language/syntax.rq".attribute_namespacing]
// rq:["../../../reqlan rq/language/syntax.rq".references_to_subidea]
type QualifiedIdeaTarget = IdeaDeclaration | Attribute;

QualifiedReference:
(path=[Import:STRING] | qualifier=[Import:ID]) '.'
(ideaset=[IdeaSet:ReferenceName] '.')? idea=[IdeaDeclaration:ReferenceName];
(ideaset=[IdeaSet:ReferenceName] '.')? idea=[QualifiedIdeaTarget:ReferenceName];

LocalReference:
idea=[IdeaDeclaration:ReferenceName];
Expand Down
37 changes: 23 additions & 14 deletions packages/language/test/linking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,14 +1528,19 @@ example_ideaset (
.filter(isQualifiedReference);
expect(qualified).toHaveLength(2);

const parsedDocument = document;
if (parsedDocument === undefined) {
throw new Error('The parsed document is missing.');
}

const attributeRef = qualified[0];
expect(attributeRef.idea?.ref).toBeDefined();
expect(isAttribute(attributeRef.idea?.ref)).toBe(true);
if (!isAttribute(attributeRef.idea?.ref)) {
return;
const attributeTarget = attributeRef?.idea?.ref;
expect(attributeTarget).toBeDefined();
if (!isAttribute(attributeTarget)) {
throw new Error('The link target is not an attribute.');
}
expect(attributeRef.idea.ref.name).toBe('example');
const owner = attributeRef.idea.ref.$container;
expect(attributeTarget.name).toBe('example');
const owner = attributeTarget.$container;
expect(isIdea(owner) && owner.name).toBe('attribute_namespacing');

const memberRef = qualified[1];
Expand Down Expand Up @@ -1566,11 +1571,11 @@ example_ideaset (
});
expect(targetLine).toContain('@example');

const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(document, {
textDocument: { uri: document.textDocument.uri }
const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(parsedDocument, {
textDocument: { uri: parsedDocument.textDocument.uri }
});
const attributeLink = documentLinks?.find(entry =>
document.textDocument.getText(entry.range) === 'example'
parsedDocument.textDocument.getText(entry.range) === 'example'
&& entry.target?.includes(`#L${links![0].targetSelectionRange!.start.line + 1}`)
);
expect(attributeLink?.target).toBeDefined();
Expand All @@ -1590,22 +1595,26 @@ example_ideaset (
}
`);
await services.shared.workspace.DocumentBuilder.build([document], { validation: true });
const parsedDocument = document;
if (parsedDocument === undefined) {
throw new Error('The parsed document is missing.');
}

const messages = (document.diagnostics ?? [])
const messages = (parsedDocument.diagnostics ?? [])
.map(diagnostic => diagnostic.message)
.filter((message): message is string => typeof message === 'string');
expect(messages).toContain("Could not resolve attribute 'missing' on idea 'attribute_namespacing'.");
expect(messages.some(message => message.includes("IdeaDeclaration named 'missing'"))).toBe(false);

const qualified = [...AstUtils.streamAst(document.parseResult.value)].filter(isQualifiedReference);
const qualified = [...AstUtils.streamAst(parsedDocument.parseResult.value)].filter(isQualifiedReference);
expect(qualified).toHaveLength(1);
expect(isIdea(peekResolvedRef(qualified[0].idea))).toBe(false);

const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(document, {
textDocument: { uri: document.textDocument.uri }
const documentLinks = await services.Reqlan.lsp.DocumentLinkProvider?.getDocumentLinks(parsedDocument, {
textDocument: { uri: parsedDocument.textDocument.uri }
});
const decoyLink = documentLinks?.find(entry =>
document.textDocument.getText(entry.range) === 'missing'
parsedDocument.textDocument.getText(entry.range) === 'missing'
);
expect(decoyLink).toBeUndefined();
});
Expand Down
2 changes: 1 addition & 1 deletion reqlan rq/core_analysis/check.rq
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ check_qualified_missing_file {
}

check_qualified_missing_idea {
A qualified [reference] names a file and an idea. Example: `["./graph-analysers.rq".index_ideas]`.
A qualified [reference] names a file and an idea. Example: `["../extension/index-host/graph-analysers.rq".not_an_idea]`.
The same rule applies to a namespace alias: `[alias.idea]` ([import_namespace]).
[check] reports an error when that file is an `.rq` file, the file does not declare the idea, and the file does not bind that name as an import alias.
The issue kind is `references`. The label is the missing idea name.
Expand Down
2 changes: 1 addition & 1 deletion reqlan rq/extension/extension.rq
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ extension_product {

host_lifecycle {
Install vs base, activation, first paint, onboarding, and configuration schema contribution.
[installation.installation], [activation.activation], [startup.startup_performance],
[installation.installation], [activation.activation], [startup.extension_startup_reliability],
[onboarding.installation_event], [configuration.configuration_location],
[command_palette_doc.command_palette].
@status done
Expand Down
2 changes: 1 addition & 1 deletion reqlan rq/extension/index-host/index.rq
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ application_memory {
Base-local application memory for reqlan tools lives under `<base>/.reqlan/`.
The presence of `.reqlan` **marks** a directory as a ["../../bases/base.rq".base]; each base owns its own store.
Shared by the VS Code extension, ["../../cli/cli_package.rq".cli_package], and MCP ([mcp_server]) — not VS Code `globalStorageUri` / extension-private storage.
The ideas graph index (["./graph-analysers.rq".index_ideas] / ["./graph-analysers.rq".index_technology]) persists as `ideas-index.sqlite` inside that directory.
The ideas graph index ([indexer.index_ideas] and [indexer.index_technology]) persists as `ideas-index.sqlite` inside that directory.
Schema: ["../../indexer/sqlite-schema.rq".ideas_index_sqlite].
Index timing diagnostics persist separately as `index-diagnostics.sqlite` ([index_diagnostics_store] / ["./index-diagnostics.rq".index_diagnostics]) so rebuilds of the ideas index do not wipe history.
Click session state persists as `click-sessions.sqlite` (["../../cli/click.rq".click_session]) so agent/CLI click cursors survive ideas-index rebuilds.
Expand Down
6 changes: 3 additions & 3 deletions reqlan rq/extension/local-context/activitybar.rq
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ header_buttons {
- "Sync with editor" toggle: panes follow the active editor and cursor by default; when off, stay pinned on the last focused idea.
- [hop_depth_control]: global graph hop depth for context search, graph slice, references, and ancestors — not a boolean toggle.
- "Open in Ideas Summary": reveal [ideas_summary_panel] with the ideas table filtered to the current file, or the graph tab centred on the current idea.
- "Export slice": JSON or CSV of the current neighbourhood only (lighter than workspace [commands.export_json] / [commands.export_csv]).
- "Export slice": JSON or CSV of the current neighbourhood only. This slice is lighter than workspace ["../export/export.rq".export_JSON] and ["../export/export.rq".export_csv].
- "Copy as markdown": copy the focused idea plus parents and reference lists for pasting into chat or PRs.
- Refresh control tied to index readiness.
- [info_site_link]
Expand Down Expand Up @@ -126,7 +126,7 @@ hop_depth_control {
- Minimum hop depth is 1 (direct neighbours only); **-** is disabled at minimum.
- Maximum hop depth is capped (default 4 for the activity bar); **+** is disabled at maximum.
- Changing global depth rebuilds [context_scope_doc.reqlan_context_model], refreshes graph/refs/ancestors panes, and updates [context_scope_doc.context_footprint].
- When no [context_scope_doc.effective_center_id] exists, depth still updates session state; panes refresh when a centre appears.
- When [context_scope_doc.context_footprint] has no centre, depth still updates session state. Panes refresh when a centre appears.
- Legacy `setIncludeIndirect` maps to depth 2/1 for backward compatibility during migration.
}

Expand Down Expand Up @@ -296,7 +296,7 @@ component_list {
- IdeasetPane, FileReferencesPane, RecentPane
- [graph_path_doc.graph_path_walk] UI pane
- ExportSliceButton
- Full workspace export via [commands.export_json] / [commands.export_csv]
- Full workspace export via ["../export/export.rq".export_JSON] and ["../export/export.rq".export_csv]
}

minigraph_file_reference_links {
Expand Down
4 changes: 4 additions & 0 deletions reqlan rq/language/syntax.rq
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ attribute_namespacing {
A bracket link `[idea.attribute]` targets one attribute of that idea.
The first name is the idea name.
The second name is the attribute name.
The grammar types that name as an attribute or an idea declaration.
`[attribute_namespacing.example]` targets the `example` attribute of this idea.
The form `[ideaset.member]` still targets an ideaset member.
See [references_to_subidea].
Expand All @@ -397,11 +398,14 @@ attribute_namespacing {
Related: [reference_brackets], [attribute_marker], ["../core_analysis/html_export.rq".html_export_internal_links].
@status done
@implementation (
["../../packages/language/src/reqlan.langium"]
["../../crates/reqlan-parse/src/lexer.rs"]
["../../crates/reqlan-parse/src/parser.rs"]
["../../packages/language/src/reqlan-scope.ts"]
["../../packages/language/src/reqlan-linker.ts"]
["../../packages/language/src/reqlan-attribute-links.ts"]
["../../packages/language/src/reqlan-references.ts"]
["../../packages/language/src/reqlan-reference-code-lens.ts"]
["../../packages/language/src/reqlan-document-link-provider.ts"]
["../../packages/language/src/reqlan-local-symbolic-links.ts"]
["../../packages/language/src/reqlan-outbound-presentation.ts"]
Expand Down
Loading