0.1.9
This commit is contained in:
+73
-16
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import { parseXml, type XmlElement } from "../language/xmlParser";
|
||||
import type { XmlElement } from "../language/xmlParser";
|
||||
import {
|
||||
analyzeContext,
|
||||
splitListValuePrefix,
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import * as model from "../model/schemaModel";
|
||||
import { isLocalReferenceAttribute } from "../indexer/refs";
|
||||
import {
|
||||
findContainingGameObject,
|
||||
collectLocalIds,
|
||||
type LogicalElement,
|
||||
} from "../indexer/logicalTree";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex, AssetDef } from "../indexer/types";
|
||||
|
||||
@@ -21,11 +26,13 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
position: vscode.Position,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.CompletionItem[]> {
|
||||
if (!this.ws.isRa3Workspace()) return [];
|
||||
const text = document.getText();
|
||||
const offset = document.offsetAt(position);
|
||||
const doc = parseXml(text);
|
||||
const scope = await this.ws.getScope(document);
|
||||
const doc = scope.expanded;
|
||||
const ctx = analyzeContext(doc, text, offset);
|
||||
const idx = this.ws.index;
|
||||
const idx = scope.merged;
|
||||
|
||||
switch (ctx.kind) {
|
||||
case "element-name":
|
||||
@@ -33,9 +40,9 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
case "attribute-name":
|
||||
return this.attributeNameItems(ctx, document, position);
|
||||
case "attribute-value":
|
||||
return idx ? this.valueItems(ctx, document, position, idx) : [];
|
||||
return this.valueItems(ctx, document, position, idx);
|
||||
case "content":
|
||||
return idx ? this.contentItems(ctx, document, position, idx) : [];
|
||||
return this.contentItems(ctx, document, position, idx);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -177,7 +184,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
ctx: CompletionContext,
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
idx: ModIndex,
|
||||
idx: ModIndex | null,
|
||||
): vscode.CompletionItem[] {
|
||||
const el = ctx.element;
|
||||
const attr = ctx.attr;
|
||||
@@ -232,6 +239,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
);
|
||||
}
|
||||
if (isInclude && attrName === "source") {
|
||||
if (!idx) return [];
|
||||
return this.includeSourceItems(idx, prefix, make);
|
||||
}
|
||||
if (attrName === "xai:joinaction" || attrName === "joinaction") {
|
||||
@@ -242,16 +250,19 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
|
||||
// inheritFrom: same element type first, then everything.
|
||||
if (attrName === "inheritfrom") {
|
||||
if (!idx) return [];
|
||||
return this.assetIdItems(idx, el.name, null, prefix, make);
|
||||
}
|
||||
|
||||
// `id` attributes are definitions and Poid attributes are pipeline-local
|
||||
// references; offering global asset ids for them would be wrong.
|
||||
if (attrInfo && isLocalReferenceAttribute(elType, attr.name)) {
|
||||
return [];
|
||||
if (attrName === "id") return [];
|
||||
return this.localIdItems(el as LogicalElement, prefix, make);
|
||||
}
|
||||
|
||||
if (attrInfo?.refType) {
|
||||
if (!idx) return [];
|
||||
return this.assetIdItems(idx, null, attrInfo.refType, prefix, make);
|
||||
}
|
||||
if (attrInfo?.enumValues?.length) {
|
||||
@@ -264,7 +275,7 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
.filter((v) => v.startsWith(prefix.toLowerCase()))
|
||||
.map((v) => make(v, vscode.CompletionItemKind.Value, "boolean"));
|
||||
}
|
||||
if (attrInfo?.allowsDefine) {
|
||||
if (idx && attrInfo?.allowsDefine) {
|
||||
return this.defineItems(idx, prefix, make);
|
||||
}
|
||||
return [];
|
||||
@@ -304,8 +315,12 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
): vscode.CompletionItem[] {
|
||||
const lower = prefix.toLowerCase();
|
||||
const scored: { def: AssetDef; score: number }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const consider = (def: AssetDef) => {
|
||||
const key = `${def.type}:${def.id.toLowerCase()}:${def.file}:${def.line}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
if (!def.id.toLowerCase().startsWith(lower)) return;
|
||||
let score = 3;
|
||||
if (refType && model.isAssignableTo(def.type, refType)) score = 1;
|
||||
@@ -315,6 +330,18 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
};
|
||||
|
||||
const targetType = selfType ?? refType;
|
||||
const localAssets = idx.local?.assets;
|
||||
const localById = idx.local?.assetsById;
|
||||
if (localAssets || localById) {
|
||||
if (!targetType) {
|
||||
for (const list of localById!.values()) for (const d of list) consider(d);
|
||||
} else {
|
||||
for (const [typeName, byId] of localAssets!) {
|
||||
if (!model.isAssignableTo(typeName, targetType)) continue;
|
||||
for (const list of byId.values()) for (const d of list) consider(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetType) {
|
||||
for (const list of idx.assetsById.values()) for (const d of list) consider(d);
|
||||
} else {
|
||||
@@ -343,24 +370,54 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
): vscode.CompletionItem[] {
|
||||
const lower = prefix.replace(/^[=$]*/, "").toLowerCase();
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
for (const [key, defs] of idx.defines) {
|
||||
if (!key.includes(lower)) continue;
|
||||
const def = defs[0];
|
||||
const label = `$${def.name}`;
|
||||
const item = make(label, vscode.CompletionItemKind.Constant, "Define", def.value);
|
||||
item.insertText = label;
|
||||
items.push(item);
|
||||
const seen = new Set<string>();
|
||||
for (const defines of [idx.local?.defines, idx.defines]) {
|
||||
if (!defines) continue;
|
||||
for (const [key, defs] of defines) {
|
||||
if (!key.includes(lower)) continue;
|
||||
const def = defs[0];
|
||||
const dedupe = `${def.name.toLowerCase()}:${def.file}:${def.line}`;
|
||||
if (seen.has(dedupe)) continue;
|
||||
seen.add(dedupe);
|
||||
const label = `$${def.name}`;
|
||||
const item = make(label, vscode.CompletionItemKind.Constant, "Define", def.value);
|
||||
item.insertText = label;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
return items.slice(0, MAX_VALUE_ITEMS);
|
||||
}
|
||||
|
||||
private localIdItems(
|
||||
el: LogicalElement,
|
||||
prefix: string,
|
||||
make: (label: string, kind: vscode.CompletionItemKind, detail: string, doc?: string) => vscode.CompletionItem,
|
||||
): vscode.CompletionItem[] {
|
||||
const root = findContainingGameObject(el);
|
||||
if (!root) return [];
|
||||
const lower = prefix.toLowerCase();
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
for (const { id } of collectLocalIds(root)) {
|
||||
if (!id.toLowerCase().startsWith(lower)) continue;
|
||||
items.push(
|
||||
make(
|
||||
id,
|
||||
vscode.CompletionItemKind.Value,
|
||||
"local module",
|
||||
"Pipeline-local id in the enclosing GameObject (includes xi:include targets).",
|
||||
),
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// ── Element content ───────────────────────────────────────────────
|
||||
|
||||
private contentItems(
|
||||
ctx: CompletionContext,
|
||||
_document: vscode.TextDocument,
|
||||
_position: vscode.Position,
|
||||
_idx: ModIndex,
|
||||
_idx: ModIndex | null,
|
||||
): vscode.CompletionItem[] {
|
||||
const el = ctx.element;
|
||||
if (!el) return [];
|
||||
|
||||
+78
-25
@@ -1,6 +1,6 @@
|
||||
import * as vscode from "vscode";
|
||||
import { dirname } from "node:path";
|
||||
import { LineMap, parseXml, type XmlElement } from "../language/xmlParser";
|
||||
import { LineMap, type XmlElement } from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import { resolveSource, buildSearchPaths } from "../indexer/includeResolver";
|
||||
import * as model from "../model/schemaModel";
|
||||
@@ -8,8 +8,11 @@ import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex } from "../indexer/types";
|
||||
import {
|
||||
isReferenceAttributeOfType,
|
||||
mergeLocalAndGlobalDefs,
|
||||
resolveReferenceTargetsForType,
|
||||
} from "../indexer/refs";
|
||||
import type { LogicalElement } from "../indexer/logicalTree";
|
||||
import { scopePathKey } from "../indexer/localScope";
|
||||
|
||||
export class Ra3Diagnostics {
|
||||
private collection: vscode.DiagnosticCollection;
|
||||
@@ -19,15 +22,19 @@ export class Ra3Diagnostics {
|
||||
}
|
||||
|
||||
async update(document: vscode.TextDocument): Promise<void> {
|
||||
const idx = this.ws.index;
|
||||
if (!idx) {
|
||||
if (!this.ws.isRa3Workspace()) {
|
||||
this.collection.set(document.uri, []);
|
||||
return;
|
||||
}
|
||||
const scope = await this.ws.getScope(document);
|
||||
const idx = scope.merged;
|
||||
const text = document.getText();
|
||||
const lineMap = new LineMap(text);
|
||||
const doc = parseXml(text);
|
||||
const doc = scope.parse;
|
||||
const diags: vscode.Diagnostic[] = [];
|
||||
// Reference/duplicate checks are provisional while the index is
|
||||
// incomplete or stale: "not found" may be a false positive.
|
||||
const provisional = idx ? !idx.complete || idx.stale === true : false;
|
||||
|
||||
for (const err of doc.errors) {
|
||||
diags.push(
|
||||
@@ -44,7 +51,15 @@ export class Ra3Diagnostics {
|
||||
}
|
||||
|
||||
if (doc.root) {
|
||||
this.checkElements(doc.root, doc, lineMap, idx, document, diags);
|
||||
this.checkElements(
|
||||
scope.expanded.root,
|
||||
scope.expanded,
|
||||
lineMap,
|
||||
idx,
|
||||
document,
|
||||
diags,
|
||||
provisional,
|
||||
);
|
||||
}
|
||||
|
||||
this.collection.set(document.uri, diags);
|
||||
@@ -59,19 +74,29 @@ export class Ra3Diagnostics {
|
||||
}
|
||||
|
||||
private checkElements(
|
||||
root: XmlElement,
|
||||
doc: { elements: XmlElement[] },
|
||||
root: LogicalElement | null,
|
||||
doc: { elements: LogicalElement[] },
|
||||
lineMap: LineMap,
|
||||
idx: ModIndex,
|
||||
idx: ModIndex | null,
|
||||
document: vscode.TextDocument,
|
||||
diags: vscode.Diagnostic[],
|
||||
provisional: boolean,
|
||||
): void {
|
||||
const settings = this.ws.settings;
|
||||
const fileDuplicates = new Map<string, { line: number }>();
|
||||
|
||||
for (const el of doc.elements) {
|
||||
// Only report diagnostics for nodes that belong to the document being
|
||||
// edited. Nodes spliced in through xi:include keep their own source
|
||||
// file and are diagnosed when that file is opened.
|
||||
if (scopePathKey(el.sourceFile) !== scopePathKey(document.uri.fsPath)) {
|
||||
continue;
|
||||
}
|
||||
const local = localName(el.name);
|
||||
const isTopLevel = el.parent === root && !["Tags", "Includes", "Defines"].includes(local);
|
||||
const isTopLevel =
|
||||
root !== null &&
|
||||
el.parent === root &&
|
||||
!["Tags", "Includes", "Defines"].includes(local);
|
||||
const range = tagRange(document, el);
|
||||
|
||||
// Top-level assets must have an id.
|
||||
@@ -111,6 +136,7 @@ export class Ra3Diagnostics {
|
||||
document,
|
||||
idx,
|
||||
diags,
|
||||
provisional,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +195,7 @@ export class Ra3Diagnostics {
|
||||
document,
|
||||
idx,
|
||||
diags,
|
||||
provisional,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -184,12 +211,17 @@ export class Ra3Diagnostics {
|
||||
type: string,
|
||||
id: string,
|
||||
document: vscode.TextDocument,
|
||||
idx: ModIndex,
|
||||
idx: ModIndex | null,
|
||||
diags: vscode.Diagnostic[],
|
||||
provisional: boolean,
|
||||
): void {
|
||||
if (!idx) return;
|
||||
const byType = idx.assets.get(type);
|
||||
const defs = byType?.get(id.toLowerCase());
|
||||
if (!defs || defs.length < 2) return;
|
||||
const defs = mergeLocalAndGlobalDefs(
|
||||
idx.local?.assets.get(type)?.get(id.toLowerCase()),
|
||||
byType?.get(id.toLowerCase()),
|
||||
);
|
||||
if (defs.length < 2) return;
|
||||
const self = defs.filter(
|
||||
(d) =>
|
||||
d.origin === "project" &&
|
||||
@@ -211,7 +243,8 @@ export class Ra3Diagnostics {
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Duplicate id "${id}" for <${type}> (also defined in ${other.file})`,
|
||||
`Duplicate id "${id}" for <${type}> (also defined in ${other.file})` +
|
||||
(provisional ? " (based on a partial index)" : ""),
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"duplicate-id",
|
||||
),
|
||||
@@ -225,8 +258,9 @@ export class Ra3Diagnostics {
|
||||
value: string,
|
||||
attr: { valueStart: number; valueEnd: number },
|
||||
document: vscode.TextDocument,
|
||||
idx: ModIndex,
|
||||
idx: ModIndex | null,
|
||||
diags: vscode.Diagnostic[],
|
||||
provisional: boolean,
|
||||
): void {
|
||||
if (!value) return;
|
||||
const range = new vscode.Range(
|
||||
@@ -238,13 +272,19 @@ export class Ra3Diagnostics {
|
||||
const defineRe = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = defineRe.exec(value)) !== null) {
|
||||
if (!idx.defines.has(m[1].toLowerCase())) {
|
||||
if (
|
||||
idx &&
|
||||
!(idx.local?.defines.has(m[1].toLowerCase()) ??
|
||||
idx.defines.has(m[1].toLowerCase()))
|
||||
) {
|
||||
const code = provisional ? "undefined-define-indexing" : "undefined-define";
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
`Undefined define "$${m[1]}"`,
|
||||
`Undefined define "$${m[1]}"` +
|
||||
(provisional ? " (index incomplete — may be a false positive)" : ""),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"undefined-define",
|
||||
code,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -253,10 +293,13 @@ export class Ra3Diagnostics {
|
||||
if (value.startsWith("$") || value.startsWith("=")) return;
|
||||
const severity = this.ws.settings.reportUnresolvedReferences;
|
||||
if (severity === "none") return;
|
||||
if (!idx) return;
|
||||
if (!isReferenceAttributeOfType(elType, attrName)) return;
|
||||
const targets = resolveReferenceTargetsForType(idx, elType, attrName, value);
|
||||
if (targets.length) return;
|
||||
const anyDef = idx.assetsById.has(value.toLowerCase());
|
||||
const anyDef =
|
||||
(idx.local?.assetsById.has(value.toLowerCase()) ?? false) ||
|
||||
idx.assetsById.has(value.toLowerCase());
|
||||
const attrRef = model
|
||||
.attributesOfType(elType)
|
||||
.find((a) => a.name === attrName);
|
||||
@@ -265,16 +308,20 @@ export class Ra3Diagnostics {
|
||||
: attrRef?.isRef
|
||||
? "of the expected declared type"
|
||||
: "matching";
|
||||
const code = provisional ? "unresolved-reference-indexing" : "unresolved-reference";
|
||||
const baseMessage = anyDef
|
||||
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
|
||||
: `Unresolved reference "${value}" (not found in the current index)`;
|
||||
diags.push(
|
||||
this.diag(
|
||||
range,
|
||||
anyDef
|
||||
? `Reference "${value}" has no definition ${expected} (ids with the same name exist for other types)`
|
||||
: `Unresolved reference "${value}" (not found in the current index)`,
|
||||
provisional
|
||||
? `${baseMessage} (index incomplete — may be a false positive)`
|
||||
: baseMessage,
|
||||
severity === "warning"
|
||||
? vscode.DiagnosticSeverity.Warning
|
||||
: vscode.DiagnosticSeverity.Information,
|
||||
"unresolved-reference",
|
||||
code,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -282,7 +329,7 @@ export class Ra3Diagnostics {
|
||||
private checkInclude(
|
||||
el: XmlElement,
|
||||
document: vscode.TextDocument,
|
||||
idx: ModIndex,
|
||||
idx: ModIndex | null,
|
||||
diags: vscode.Diagnostic[],
|
||||
): void {
|
||||
const typeAttr = el.attrs.find((a) => a.name === "type");
|
||||
@@ -301,12 +348,18 @@ export class Ra3Diagnostics {
|
||||
);
|
||||
}
|
||||
if (!sourceAttr?.hasValue) return;
|
||||
const searchPaths = idx
|
||||
? buildSearchPaths(idx.sdkDir, idx.projectDir)
|
||||
: this.ws.searchPaths();
|
||||
if (!searchPaths) return;
|
||||
const resolved = resolveSource(
|
||||
sourceAttr.value,
|
||||
dirname(document.uri.fsPath),
|
||||
buildSearchPaths(idx.sdkDir, idx.projectDir),
|
||||
searchPaths,
|
||||
);
|
||||
if (!resolved.path && !idx.sourceCandidates.some((c) => c.source === sourceAttr.value)) {
|
||||
const candidateHit =
|
||||
idx?.sourceCandidates.some((c) => c.source === sourceAttr.value) ?? false;
|
||||
if (!resolved.path && !candidateHit) {
|
||||
diags.push(
|
||||
this.diag(
|
||||
new vscode.Range(
|
||||
|
||||
+62
-11
@@ -1,13 +1,19 @@
|
||||
import * as vscode from "vscode";
|
||||
import { findElementAt, parseXml } from "../language/xmlParser";
|
||||
import { findElementAt } from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import * as model from "../model/schemaModel";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex } from "../indexer/types";
|
||||
import {
|
||||
isLocalReferenceAttribute,
|
||||
isReferenceAttributeOfType,
|
||||
resolveReferenceTargetsForType,
|
||||
} from "../indexer/refs";
|
||||
import {
|
||||
findContainingGameObject,
|
||||
findLocalId,
|
||||
type LogicalElement,
|
||||
} from "../indexer/logicalTree";
|
||||
import { scopePathKey, type DocumentScope } from "../indexer/localScope";
|
||||
import { dirname } from "node:path";
|
||||
import { buildSearchPaths, resolveSource } from "../indexer/includeResolver";
|
||||
|
||||
@@ -19,9 +25,10 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
position: vscode.Position,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.Hover | null> {
|
||||
const text = document.getText();
|
||||
if (!this.ws.isRa3Workspace()) return null;
|
||||
const offset = document.offsetAt(position);
|
||||
const doc = parseXml(text);
|
||||
const scope = await this.ws.getScope(document);
|
||||
const doc = scope.expanded;
|
||||
const el = findElementAt(doc, offset);
|
||||
if (!el) return null;
|
||||
const elType = resolveElementType(el);
|
||||
@@ -35,7 +42,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
// Attribute value.
|
||||
for (const attr of el.attrs) {
|
||||
if (attr.hasValue && offset >= attr.valueStart && offset <= attr.valueEnd) {
|
||||
return this.valueHover(el, elType, attr.name, attr.value, document, this.ws.index);
|
||||
return this.valueHover(el, elType, attr.name, attr.value, document, scope);
|
||||
}
|
||||
}
|
||||
// Element name.
|
||||
@@ -116,14 +123,17 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
attrName: string,
|
||||
value: string,
|
||||
document: vscode.TextDocument,
|
||||
idx: ModIndex | null,
|
||||
scope: DocumentScope,
|
||||
): vscode.Hover | null {
|
||||
const idx = scope.merged;
|
||||
const md = new vscode.MarkdownString();
|
||||
|
||||
// $DEFINE reference.
|
||||
const defineMatch = /\$([A-Za-z_][A-Za-z0-9_]*)/.exec(value);
|
||||
if (defineMatch && idx) {
|
||||
const defs = idx.defines.get(defineMatch[1].toLowerCase());
|
||||
const defs =
|
||||
idx.local?.defines.get(defineMatch[1].toLowerCase()) ??
|
||||
idx.defines.get(defineMatch[1].toLowerCase());
|
||||
if (defs?.length) {
|
||||
const d = defs[0];
|
||||
md.appendMarkdown(`**Define** \`$${d.name}\` \n`);
|
||||
@@ -139,11 +149,14 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
(el.name === "Include" && attrName === "source") ||
|
||||
(el.name === "xi:include" && attrName === "href")
|
||||
) {
|
||||
const resolved = idx
|
||||
const searchPaths = idx
|
||||
? buildSearchPaths(idx.sdkDir, idx.projectDir)
|
||||
: this.ws.searchPaths();
|
||||
const resolved = searchPaths
|
||||
? resolveSource(
|
||||
value,
|
||||
dirname(document.uri.fsPath),
|
||||
buildSearchPaths(idx.sdkDir, idx.projectDir),
|
||||
searchPaths,
|
||||
).path
|
||||
: null;
|
||||
if (resolved) {
|
||||
@@ -161,8 +174,26 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
|
||||
// Pipeline-local (Poid) references: resolve inside the enclosing
|
||||
// GameObject's logical subtree (including xi:include targets).
|
||||
if (
|
||||
isLocalReferenceAttribute(elType, attrName) &&
|
||||
attrName.toLowerCase() !== "id"
|
||||
) {
|
||||
return this.localIdHover(scope, el as LogicalElement, value, document);
|
||||
}
|
||||
|
||||
// Asset reference / inheritFrom.
|
||||
if (idx) {
|
||||
if (!idx) {
|
||||
if (isReferenceAttributeOfType(elType, attrName)) {
|
||||
md.appendMarkdown(
|
||||
"Index is still building — references cannot be resolved yet.",
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
{
|
||||
if (!isReferenceAttributeOfType(elType, attrName)) return null;
|
||||
const targets = resolveReferenceTargetsForType(idx, elType, attrName, value);
|
||||
if (targets.length) {
|
||||
@@ -191,8 +222,28 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private localIdHover(
|
||||
scope: DocumentScope,
|
||||
el: LogicalElement,
|
||||
value: string,
|
||||
document: vscode.TextDocument,
|
||||
): vscode.Hover | null {
|
||||
const root = findContainingGameObject(el);
|
||||
if (!root) return null;
|
||||
const target = findLocalId(root, value);
|
||||
if (!target) return null;
|
||||
const idAttr = target.attrs.find((a) => a.name === "id");
|
||||
if (!idAttr?.hasValue) return null;
|
||||
const lineMap = scope.lineMaps.get(scopePathKey(target.sourceFile));
|
||||
const line = lineMap ? lineMap.positionAt(idAttr.valueStart).line + 1 : 0;
|
||||
const md = new vscode.MarkdownString();
|
||||
md.appendMarkdown(`**Local pipeline id** \`${value}\` \n`);
|
||||
md.appendCodeblock(`<${target.name}>`);
|
||||
const rel = relativePath(document, target.sourceFile);
|
||||
md.appendMarkdown(`Defined in \`${rel}:${line}\``);
|
||||
return new vscode.Hover(md);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+113
-16
@@ -7,7 +7,16 @@ import {
|
||||
resolveSource,
|
||||
type SearchPaths,
|
||||
} from "../indexer/includeResolver";
|
||||
import { resolveReferenceTargetsForType } from "../indexer/refs";
|
||||
import {
|
||||
isLocalReferenceAttribute,
|
||||
resolveReferenceTargetsForType,
|
||||
} from "../indexer/refs";
|
||||
import {
|
||||
findContainingGameObject,
|
||||
findLocalId,
|
||||
type LogicalElement,
|
||||
} from "../indexer/logicalTree";
|
||||
import { scopePathKey, type DocumentScope } from "../indexer/localScope";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { AssetDef, ModIndex } from "../indexer/types";
|
||||
|
||||
@@ -25,11 +34,11 @@ export class Ra3DefinitionProvider implements vscode.DefinitionProvider {
|
||||
position: vscode.Position,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.Location | vscode.Location[] | null> {
|
||||
const idx = this.ws.index;
|
||||
if (!idx) return null;
|
||||
const text = document.getText();
|
||||
if (!this.ws.isRa3Workspace()) return null;
|
||||
const scope = await this.ws.getScope(document);
|
||||
const idx = scope.merged;
|
||||
const offset = document.offsetAt(position);
|
||||
const doc = parseXml(text);
|
||||
const doc = scope.expanded;
|
||||
const el = findElementAt(doc, offset);
|
||||
if (!el) return null;
|
||||
const elType = resolveElementType(el);
|
||||
@@ -46,17 +55,31 @@ export class Ra3DefinitionProvider implements vscode.DefinitionProvider {
|
||||
(el.name === "Include" && nameLower === "source") ||
|
||||
(el.name === "include" && nameLower === "href")
|
||||
) {
|
||||
const resolved =
|
||||
resolveSource(value, dirname(document.uri.fsPath), searchPathsFor(idx)).path ??
|
||||
idx.sourceCandidates.find((c) => c.source === value)?.path ??
|
||||
null;
|
||||
return resolved
|
||||
? new vscode.Location(vscode.Uri.file(resolved), new vscode.Position(0, 0))
|
||||
const searchPaths = idx ? searchPathsFor(idx) : this.ws.searchPaths();
|
||||
const resolved = searchPaths
|
||||
? resolveSource(value, dirname(document.uri.fsPath), searchPaths).path
|
||||
: null;
|
||||
const fallback = idx?.sourceCandidates.find((c) => c.source === value)?.path;
|
||||
const target = resolved ?? fallback ?? null;
|
||||
return target
|
||||
? new vscode.Location(vscode.Uri.file(target), new vscode.Position(0, 0))
|
||||
: null;
|
||||
}
|
||||
|
||||
// Asset reference / inheritFrom (filtered by the attribute's ref type).
|
||||
if (!idx) return null;
|
||||
if (value && !value.startsWith("$")) {
|
||||
if (
|
||||
isLocalReferenceAttribute(elType, attr.name) &&
|
||||
nameLower !== "id"
|
||||
) {
|
||||
const local = this.localIdLocation(
|
||||
scope,
|
||||
el as LogicalElement,
|
||||
value,
|
||||
);
|
||||
if (local) return local;
|
||||
}
|
||||
let targets = resolveReferenceTargetsForType(idx, elType, attr.name, value);
|
||||
if (!targets.length) return null;
|
||||
if (
|
||||
@@ -67,13 +90,40 @@ export class Ra3DefinitionProvider implements vscode.DefinitionProvider {
|
||||
}
|
||||
const locations: vscode.Location[] = [];
|
||||
for (const { def } of targets.slice(0, 8)) {
|
||||
const loc = await assetDefLocation(this.ws, def, idx);
|
||||
const loc = await assetDefLocation(this.ws, def, idx, scope, document);
|
||||
if (loc) locations.push(loc);
|
||||
}
|
||||
return locations.length ? locations : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private localIdLocation(
|
||||
scope: DocumentScope,
|
||||
el: LogicalElement,
|
||||
value: string,
|
||||
): vscode.Location | null {
|
||||
const root = findContainingGameObject(el);
|
||||
if (!root) return null;
|
||||
const target = findLocalId(root, value);
|
||||
if (!target) return null;
|
||||
const idAttr = target.attrs.find((a) => a.name === "id");
|
||||
if (!idAttr?.hasValue) return null;
|
||||
const lineMap = scope.lineMaps.get(scopePathKey(target.sourceFile));
|
||||
if (!lineMap) {
|
||||
return new vscode.Location(
|
||||
vscode.Uri.file(target.sourceFile),
|
||||
new vscode.Position(0, 0),
|
||||
);
|
||||
}
|
||||
return new vscode.Location(
|
||||
vscode.Uri.file(target.sourceFile),
|
||||
new vscode.Range(
|
||||
toVscodePosition(lineMap.positionAt(idAttr.valueStart)),
|
||||
toVscodePosition(lineMap.positionAt(idAttr.valueEnd)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +135,8 @@ async function assetDefLocation(
|
||||
ws: ModWorkspace,
|
||||
def: AssetDef,
|
||||
idx: ModIndex,
|
||||
scope: DocumentScope,
|
||||
currentDocument: vscode.TextDocument,
|
||||
): Promise<vscode.Location | null> {
|
||||
if (def.origin === "manifest") {
|
||||
const src = def.manifestSource;
|
||||
@@ -100,6 +152,11 @@ async function assetDefLocation(
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scopePathKey(def.file) === scopePathKey(currentDocument.uri.fsPath)) {
|
||||
const precise = locationInCurrentDocument(scope, def.id, currentDocument);
|
||||
if (precise) return precise;
|
||||
}
|
||||
|
||||
return (
|
||||
(await locationInDocument(ws, def.file, def.id)) ??
|
||||
new vscode.Location(
|
||||
@@ -109,6 +166,36 @@ async function assetDefLocation(
|
||||
);
|
||||
}
|
||||
|
||||
function locationInCurrentDocument(
|
||||
scope: DocumentScope,
|
||||
id: string,
|
||||
document: vscode.TextDocument,
|
||||
): vscode.Location | null {
|
||||
const el = scope.parse.elements.find((e) =>
|
||||
e.attrs.some(
|
||||
(a) => a.name === "id" && a.value.toLowerCase() === id.toLowerCase(),
|
||||
),
|
||||
);
|
||||
if (!el) return null;
|
||||
const idAttr = el.attrs.find((a) => a.name === "id");
|
||||
if (idAttr?.hasValue) {
|
||||
return new vscode.Location(
|
||||
document.uri,
|
||||
new vscode.Range(
|
||||
document.positionAt(idAttr.valueStart),
|
||||
document.positionAt(idAttr.valueEnd),
|
||||
),
|
||||
);
|
||||
}
|
||||
return new vscode.Location(
|
||||
document.uri,
|
||||
new vscode.Range(
|
||||
document.positionAt(el.start),
|
||||
document.positionAt(el.startTagEnd),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the precise range of an asset definition inside an XML file: the id
|
||||
* attribute value when present, otherwise the element start tag.
|
||||
@@ -118,7 +205,9 @@ async function locationInDocument(
|
||||
file: string,
|
||||
id: string,
|
||||
): Promise<vscode.Location | null> {
|
||||
const parsed = await ws.indexer?.readDocument(file);
|
||||
// readDom (not readDocument) guarantees a DOM even when the compact
|
||||
// records cache already has an entry for the file.
|
||||
const parsed = await ws.indexer?.readDom(file);
|
||||
if (parsed?.parse && parsed.lineMap) {
|
||||
const el = parsed.parse.elements.find(
|
||||
(e) =>
|
||||
@@ -156,12 +245,15 @@ function toVscodePosition(p: { line: number; character: number }): vscode.Positi
|
||||
// ── Find all references ─────────────────────────────────────────────
|
||||
|
||||
export class Ra3ReferenceProvider implements vscode.ReferenceProvider {
|
||||
constructor(private ws: ModWorkspace) {}
|
||||
|
||||
async provideReferences(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
_context: vscode.ReferenceContext,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.Location[] | null> {
|
||||
if (!this.ws.isRa3Workspace()) return null;
|
||||
const text = document.getText();
|
||||
const offset = document.offsetAt(position);
|
||||
const doc = parseXml(text);
|
||||
@@ -201,8 +293,10 @@ export class Ra3DocumentLinkProvider implements vscode.DocumentLinkProvider {
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.DocumentLink[]> {
|
||||
if (!this.ws.isRa3Workspace()) return [];
|
||||
const idx = this.ws.index;
|
||||
if (!idx) return [];
|
||||
const searchPaths = idx ? searchPathsFor(idx) : this.ws.searchPaths();
|
||||
if (!searchPaths) return [];
|
||||
const text = document.getText();
|
||||
const doc = parseXml(text);
|
||||
const links: vscode.DocumentLink[] = [];
|
||||
@@ -214,9 +308,9 @@ export class Ra3DocumentLinkProvider implements vscode.DocumentLinkProvider {
|
||||
resolveSource(
|
||||
srcAttr.value,
|
||||
dirname(document.uri.fsPath),
|
||||
searchPathsFor(idx),
|
||||
searchPaths,
|
||||
).path ??
|
||||
idx.sourceCandidates.find((c) => c.source === srcAttr.value)?.path ??
|
||||
idx?.sourceCandidates.find((c) => c.source === srcAttr.value)?.path ??
|
||||
null;
|
||||
if (!target) continue;
|
||||
links.push(
|
||||
@@ -236,10 +330,13 @@ export class Ra3DocumentLinkProvider implements vscode.DocumentLinkProvider {
|
||||
// ── Document symbols (outline) ──────────────────────────────────────
|
||||
|
||||
export class Ra3DocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
constructor(private ws: ModWorkspace) {}
|
||||
|
||||
async provideDocumentSymbols(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.DocumentSymbol[]> {
|
||||
if (!this.ws.isRa3Workspace()) return [];
|
||||
const text = document.getText();
|
||||
const doc = parseXml(text);
|
||||
const root = doc.root;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vscode from "vscode";
|
||||
import { parseXml } from "../language/xmlParser";
|
||||
import { buildSemanticTokenRanges } from "../language/semanticTokens";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
|
||||
const TOKEN_TYPES = ["type", "property", "string"] as const;
|
||||
|
||||
@@ -20,10 +21,15 @@ export const RA3_SEMANTIC_TOKENS_LEGEND = new vscode.SemanticTokensLegend([
|
||||
export class Ra3SemanticTokensProvider
|
||||
implements vscode.DocumentSemanticTokensProvider
|
||||
{
|
||||
constructor(private ws: ModWorkspace) {}
|
||||
|
||||
async provideDocumentSemanticTokens(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.SemanticTokens> {
|
||||
if (!this.ws.isRa3Workspace()) {
|
||||
return new vscode.SemanticTokens(new Uint32Array(0));
|
||||
}
|
||||
const text = document.getText();
|
||||
const doc = parseXml(text);
|
||||
if (doc.errors.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user