0.1.20
This commit is contained in:
+73
-14
@@ -2,11 +2,13 @@ import * as vscode from "vscode";
|
||||
import { LineMap, parseXml } from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import { isReferenceTargetType } from "../indexer/refs";
|
||||
import { scheduleRebuildIfRecordsDesync } from "../indexer/referenceIndex";
|
||||
import type { ModIndex } from "../indexer/types";
|
||||
import {
|
||||
referenceSitesForDefinition,
|
||||
scheduleRebuildIfRecordsDesync,
|
||||
} from "../indexer/referenceIndex";
|
||||
import type { ShowReferencesArgs } from "./references";
|
||||
collectReferenceSites,
|
||||
definitionsForReference,
|
||||
type ShowReferencesArgs,
|
||||
} from "./references";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
|
||||
/** Never build a DOM for huge files just to show counts (w3x safety). */
|
||||
@@ -21,18 +23,65 @@ const MAX_CODELENS_TEXT = 4 * 1024 * 1024;
|
||||
* signal users can click to inspect an unused asset.
|
||||
*/
|
||||
export class Ra3CodeLensProvider implements vscode.CodeLensProvider {
|
||||
private changeEmitter = new vscode.EventEmitter<void>();
|
||||
readonly onDidChangeCodeLenses = this.changeEmitter.event;
|
||||
/** URIs for which "no global snapshot yet" has already been logged. */
|
||||
private suppressedLogged = new Set<string>();
|
||||
|
||||
constructor(private ws: ModWorkspace) {}
|
||||
|
||||
provideCodeLenses(
|
||||
/** Tells VS Code to re-query lenses (used after index snapshots). */
|
||||
refresh(): void {
|
||||
this.changeEmitter.fire();
|
||||
}
|
||||
|
||||
/** Called when a new snapshot is published; allows re-logging suppression. */
|
||||
resetSuppressionLog(): void {
|
||||
this.suppressedLogged.clear();
|
||||
}
|
||||
|
||||
async provideCodeLenses(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken,
|
||||
): vscode.CodeLens[] {
|
||||
): Promise<vscode.CodeLens[]> {
|
||||
if (!this.ws.isRa3Workspace()) return [];
|
||||
const idx = this.ws.index;
|
||||
const startedAt = Date.now();
|
||||
const uri = document.uri.toString();
|
||||
let idx: ModIndex | null = null;
|
||||
try {
|
||||
idx = (await this.ws.getCodeLensScope(document)).merged;
|
||||
} catch (err) {
|
||||
this.ws.log(
|
||||
`[codelens] scope error for ${uri}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
if (!idx) return [];
|
||||
// Before the first global snapshot exists the merged index is a
|
||||
// local-only index (stats.indexedFiles === 0) with no real references.
|
||||
// Rendering "0 references" then would be misleading, so wait until a
|
||||
// snapshot is published. Once a snapshot exists, "0" is meaningful and
|
||||
// must still be displayed for reference-target types.
|
||||
if (!idx.complete && idx.stats.indexedFiles === 0) {
|
||||
if (!this.suppressedLogged.has(uri)) {
|
||||
this.suppressedLogged.add(uri);
|
||||
this.ws.log(
|
||||
`[codelens] suppressed for ${uri} (no global snapshot yet)`,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
const text = document.getText();
|
||||
if (text.length > MAX_CODELENS_TEXT) return [];
|
||||
scheduleRebuildIfRecordsDesync(this.ws, document);
|
||||
if (text.length > MAX_CODELENS_TEXT) {
|
||||
this.ws.log(
|
||||
`[codelens] skipped for ${uri} (${text.length} bytes > ${MAX_CODELENS_TEXT})`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
scheduleRebuildIfRecordsDesync(
|
||||
this.ws.recordsSyncSurfaceFor(document),
|
||||
document,
|
||||
);
|
||||
const doc = parseXml(text);
|
||||
const root = doc.root;
|
||||
if (!root) return [];
|
||||
@@ -49,12 +98,16 @@ export class Ra3CodeLensProvider implements vscode.CodeLensProvider {
|
||||
|
||||
const id = idAttr.value;
|
||||
const line = lineMap.positionAt(idAttr.valueStart).line + 1;
|
||||
const count = referenceSitesForDefinition(idx, {
|
||||
type: local,
|
||||
// Same definition union as Find All References: document-local
|
||||
// overlay + every same-id definition in the global index. This keeps
|
||||
// the lens count and the references peek consistent even when the
|
||||
// file itself is not part of the global include graph.
|
||||
const defs = definitionsForReference(idx, {
|
||||
id,
|
||||
file: document.uri.fsPath,
|
||||
line,
|
||||
}).length;
|
||||
refType: null,
|
||||
selfType: null,
|
||||
});
|
||||
const count = collectReferenceSites(idx, defs).length;
|
||||
const range = new vscode.Range(
|
||||
document.positionAt(child.start),
|
||||
document.positionAt(child.startTagEnd),
|
||||
@@ -80,6 +133,12 @@ export class Ra3CodeLensProvider implements vscode.CodeLensProvider {
|
||||
}),
|
||||
);
|
||||
}
|
||||
const elapsed = Date.now() - startedAt;
|
||||
if (elapsed > 250) {
|
||||
this.ws.log(
|
||||
`[codelens] slow provider for ${uri}: ${lenses.length} lenses in ${elapsed}ms`,
|
||||
);
|
||||
}
|
||||
return lenses;
|
||||
}
|
||||
}
|
||||
|
||||
+81
-35
@@ -454,20 +454,41 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
make: (label: string, kind: vscode.CompletionItemKind, detail: string, doc?: string) => vscode.CompletionItem,
|
||||
): vscode.CompletionItem[] | vscode.CompletionList<vscode.CompletionItem> {
|
||||
const lower = prefix.toLowerCase();
|
||||
const scored: { def: AssetDef; score: number }[] = [];
|
||||
// Deduplicate by id: the same asset can be defined in several places at
|
||||
// once (current file's local overlay + global index, project XML +
|
||||
// compiled manifest, or an override). Showing one completion entry per
|
||||
// id is enough; the other definitions are listed in the documentation.
|
||||
// Definitions are still de-duplicated by (type, id, file, line) so the
|
||||
// same record found through both local and global maps is not repeated
|
||||
// inside a single entry either.
|
||||
const seen = new Set<string>();
|
||||
const byId = new Map<
|
||||
string,
|
||||
{ best: { def: AssetDef; score: number }; extras: AssetDef[] }
|
||||
>();
|
||||
|
||||
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;
|
||||
const defKey = `${def.type}:${def.id.toLowerCase()}:${def.file}:${def.line}`;
|
||||
if (seen.has(defKey)) return;
|
||||
seen.add(defKey);
|
||||
const idKey = def.id.toLowerCase();
|
||||
if (!idKey.startsWith(lower)) return;
|
||||
let score = 3;
|
||||
if (refType && model.isAssignableTo(def.type, refType)) score = 1;
|
||||
if (selfType && model.isAssignableTo(def.type, selfType)) score = 0;
|
||||
if (def.origin === "project") score -= 0.2;
|
||||
if (def.stream === "local") score -= 0.4;
|
||||
scored.push({ def, score });
|
||||
const entry = byId.get(idKey);
|
||||
if (!entry) {
|
||||
byId.set(idKey, { best: { def, score }, extras: [] });
|
||||
return;
|
||||
}
|
||||
if (score < entry.best.score) {
|
||||
entry.extras.push(entry.best.def);
|
||||
entry.best = { def, score };
|
||||
} else {
|
||||
entry.extras.push(def);
|
||||
}
|
||||
};
|
||||
|
||||
const targetType = selfType ?? refType;
|
||||
@@ -492,17 +513,28 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const top = topScoredDefs(scored, MAX_VALUE_ITEMS);
|
||||
const entries = [...byId.values()];
|
||||
const top = topScoredDefs(
|
||||
entries.map((e) => e.best),
|
||||
MAX_VALUE_ITEMS,
|
||||
);
|
||||
const items = top.map(({ def }) => {
|
||||
const origin = def.origin === "manifest" ? `manifest (${def.manifestSource ?? ""})` : def.origin;
|
||||
const originLabel = (d: AssetDef) =>
|
||||
d.origin === "manifest" ? `manifest (${d.manifestSource ?? ""})` : d.origin;
|
||||
const origin = originLabel(def);
|
||||
const doc = new vscode.MarkdownString();
|
||||
doc.appendCodeblock(def.id);
|
||||
doc.appendMarkdown(`**Type**: ${def.type} \n`);
|
||||
if (def.manifestSource) doc.appendMarkdown(`**Source**: ${def.manifestSource} \n`);
|
||||
doc.appendMarkdown(`**Origin**: ${origin}`);
|
||||
for (const extra of byId.get(def.id.toLowerCase())?.extras ?? []) {
|
||||
doc.appendMarkdown(
|
||||
`\n\nAlso defined as **${extra.type}** · ${originLabel(extra)}`,
|
||||
);
|
||||
}
|
||||
return make(def.id, vscode.CompletionItemKind.Value, `${def.type} · ${origin}`, doc.value);
|
||||
});
|
||||
return this.limitItems(items, scored.length);
|
||||
return this.limitItems(items, byId.size);
|
||||
}
|
||||
|
||||
private defineItems(
|
||||
@@ -512,13 +544,16 @@ export class Ra3CompletionProvider implements vscode.CompletionItemProvider {
|
||||
): vscode.CompletionItem[] | vscode.CompletionList<vscode.CompletionItem> {
|
||||
const lower = prefix.replace(/^[=$]*/, "").toLowerCase();
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
// The same define can be visible through both the local overlay and the
|
||||
// global index; show one entry per name (local definitions win because
|
||||
// they are iterated first).
|
||||
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}`;
|
||||
const dedupe = def.name.toLowerCase();
|
||||
if (seen.has(dedupe)) continue;
|
||||
seen.add(dedupe);
|
||||
const label = `$${def.name}`;
|
||||
@@ -816,9 +851,17 @@ function attributeInsertLayout(
|
||||
const wordStart = findAttributeWordStart(text, offset, el.start);
|
||||
const attrs = el.attrs;
|
||||
const complete = attrs.filter((a) => a.hasValue);
|
||||
const last = complete.length ? complete[complete.length - 1] : null;
|
||||
// Only attributes that end before the cursor decide whether the completed
|
||||
// attribute is already on its own line. The tag's last complete attribute
|
||||
// may still be AFTER the cursor when the user inserts a new attribute in
|
||||
// the middle of a one-per-line tag; using it here would wrongly re-wrap.
|
||||
const beforeCursor = complete.filter((a) => attributeEndOffset(a) <= offset);
|
||||
const last = beforeCursor.length ? beforeCursor[beforeCursor.length - 1] : null;
|
||||
const lastEnd = last ? attributeEndOffset(last) : -1;
|
||||
const alreadyOnNewLine = lastEnd >= 0 && text.slice(lastEnd, offset).includes("\n");
|
||||
const alreadyOnNewLine =
|
||||
lastEnd >= 0
|
||||
? text.slice(lastEnd, offset).includes("\n")
|
||||
: text.slice(el.start + 1 + el.name.length, offset).includes("\n");
|
||||
|
||||
// Canonical indent anchor: the first complete attribute that starts on its
|
||||
// own line. Fall back to the last complete attribute for inline elements.
|
||||
@@ -838,31 +881,34 @@ function attributeInsertLayout(
|
||||
? text.slice(0, anchor.nameStart).match(/[ \t]*$/)?.[0] ?? ""
|
||||
: "";
|
||||
|
||||
if (!onePerLine) {
|
||||
if (alreadyOnNewLine) {
|
||||
// Inline-style file, but the user started a new line: keep whatever
|
||||
// indentation they already typed.
|
||||
return { rangeStart: wordStart, prefix: "" };
|
||||
}
|
||||
const needsSpace = wordStart > el.start + 1 && !/\s/.test(text[wordStart - 1]);
|
||||
return { rangeStart: wordStart, prefix: needsSpace ? " " : "" };
|
||||
}
|
||||
if (alreadyOnNewLine) {
|
||||
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
||||
return { rangeStart: lineStart, prefix: indent };
|
||||
// The attribute being completed is already on its own line: never insert
|
||||
// another newline. In one-per-line files align with the canonical indent;
|
||||
// in inline files keep whatever indentation the user already typed.
|
||||
if (onePerLine) {
|
||||
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
||||
return { rangeStart: lineStart, prefix: indent };
|
||||
}
|
||||
return { rangeStart: wordStart, prefix: "" };
|
||||
}
|
||||
// Insert on a new line. The editor adds the current line's indentation to
|
||||
// the new line, so we must NOT embed our own indent here (it would
|
||||
// compound). If whitespace was typed between the previous attribute and
|
||||
// the cursor (e.g. a space used to trigger the suggestion popup), consume
|
||||
// it so it does not linger as a trailing space.
|
||||
const wsStart =
|
||||
lastEnd >= 0 &&
|
||||
wordStart > lastEnd &&
|
||||
/^[ \t]*$/.test(text.slice(lastEnd, wordStart))
|
||||
? lastEnd
|
||||
: wordStart;
|
||||
return { rangeStart: wsStart, prefix: "\n" };
|
||||
// The cursor sits on the same line as the element name or a complete
|
||||
// attribute: the completed attribute would be the second one on that line.
|
||||
if (onePerLine) {
|
||||
// Insert on a new line. The editor adds the current line's indentation
|
||||
// to the new line, so we must NOT embed our own indent here (it would
|
||||
// compound). If whitespace was typed between the previous attribute and
|
||||
// the cursor (e.g. a space used to trigger the suggestion popup), consume
|
||||
// it so it does not linger as a trailing space.
|
||||
const wsStart =
|
||||
lastEnd >= 0 &&
|
||||
wordStart > lastEnd &&
|
||||
/^[ \t]*$/.test(text.slice(lastEnd, wordStart))
|
||||
? lastEnd
|
||||
: wordStart;
|
||||
return { rangeStart: wsStart, prefix: "\n" };
|
||||
}
|
||||
const needsSpace = wordStart > el.start + 1 && !/\s/.test(text[wordStart - 1]);
|
||||
return { rangeStart: wordStart, prefix: needsSpace ? " " : "" };
|
||||
}
|
||||
|
||||
function attributeEndOffset(attr: XmlAttribute): number {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
||||
import { LineMap, type XmlElement } from "../language/xmlParser";
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import { resolveSource, buildSearchPaths } from "../indexer/includeResolver";
|
||||
import { validateSdkPath } from "../sdk";
|
||||
import * as model from "../model/schemaModel";
|
||||
import type { ModWorkspace } from "../workspace";
|
||||
import type { ModIndex } from "../indexer/types";
|
||||
@@ -18,11 +19,22 @@ import { scopePathKey } from "../indexer/localScope";
|
||||
|
||||
export class Ra3Diagnostics {
|
||||
private collection: vscode.DiagnosticCollection;
|
||||
private sdkCache: { path: string; unusable: boolean } | null = null;
|
||||
|
||||
constructor(private ws: ModWorkspace) {
|
||||
this.collection = vscode.languages.createDiagnosticCollection("ra3modxml");
|
||||
}
|
||||
|
||||
/** True when the SDK is missing or not an SDK root (project-only mode). */
|
||||
private sdkUnusable(): boolean {
|
||||
const path = this.ws.settings.sdkPath;
|
||||
if (this.sdkCache?.path === path) return this.sdkCache.unusable;
|
||||
const status = validateSdkPath(path).status;
|
||||
const unusable = status === "missing" || status === "not-sdk";
|
||||
this.sdkCache = { path, unusable };
|
||||
return unusable;
|
||||
}
|
||||
|
||||
async update(document: vscode.TextDocument): Promise<void> {
|
||||
if (!this.ws.isRa3Workspace()) {
|
||||
this.collection.set(document.uri, []);
|
||||
@@ -429,7 +441,7 @@ export class Ra3Diagnostics {
|
||||
if (!sourceAttr?.hasValue) return;
|
||||
const searchPaths = idx
|
||||
? buildSearchPaths(idx.sdkDir, idx.projectDir)
|
||||
: this.ws.searchPaths();
|
||||
: this.ws.searchPaths(document);
|
||||
if (!searchPaths) return;
|
||||
const resolved = resolveSource(
|
||||
sourceAttr.value,
|
||||
@@ -439,6 +451,11 @@ export class Ra3Diagnostics {
|
||||
const candidateHit =
|
||||
idx?.sourceCandidates.some((c) => c.source === sourceAttr.value) ?? false;
|
||||
if (!resolved.path && !candidateHit) {
|
||||
// Without a usable SDK, prefixed includes are expected to be missing;
|
||||
// report one project-level hint instead of warning on every line.
|
||||
if (this.sdkUnusable() && /^(DATA|ART|AUDIO):/i.test(sourceAttr.value.trim())) {
|
||||
return;
|
||||
}
|
||||
diags.push(
|
||||
this.diag(
|
||||
new vscode.Range(
|
||||
|
||||
@@ -155,7 +155,7 @@ export class Ra3HoverProvider implements vscode.HoverProvider {
|
||||
) {
|
||||
const searchPaths = idx
|
||||
? buildSearchPaths(idx.sdkDir, idx.projectDir)
|
||||
: this.ws.searchPaths();
|
||||
: this.ws.searchPaths(document);
|
||||
const resolved = searchPaths
|
||||
? resolveSource(
|
||||
value,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { findElementAt, parseXml, textContentTokenAt } from "../language/xmlPars
|
||||
import { resolveElementType } from "../language/typeContext";
|
||||
import {
|
||||
buildSearchPaths,
|
||||
buildVanillaSearchPaths,
|
||||
resolveSource,
|
||||
type SearchPaths,
|
||||
} from "../indexer/includeResolver";
|
||||
@@ -71,7 +72,9 @@ export class Ra3DefinitionProvider implements vscode.DefinitionProvider {
|
||||
(el.name === "Include" && nameLower === "source") ||
|
||||
(el.name === "include" && nameLower === "href")
|
||||
) {
|
||||
const searchPaths = idx ? searchPathsFor(idx) : this.ws.searchPaths();
|
||||
const searchPaths = idx
|
||||
? searchPathsFor(idx)
|
||||
: this.ws.searchPaths(document);
|
||||
const resolved = searchPaths
|
||||
? resolveSource(value, dirname(document.uri.fsPath), searchPaths).path
|
||||
: null;
|
||||
@@ -178,11 +181,23 @@ async function assetDefLocation(
|
||||
}
|
||||
if (def.origin === "manifest") {
|
||||
const src = def.manifestSource;
|
||||
if (src?.toUpperCase().startsWith("DATA:")) {
|
||||
const resolved = resolveSource(src, null, searchPathsFor(idx)).path;
|
||||
if (src) {
|
||||
// manifestSource is a path recorded by the vanilla build, not an
|
||||
// Include path in the current mod. Resolve it with SDK-only search
|
||||
// paths so a mod file shadowing the same DATA: path cannot hijack the
|
||||
// jump (e.g. mod Data/globaldata/weapon.xml vs SageXml/...). If the SDK
|
||||
// source is missing (user removed/renamed a SageXml file), keep the
|
||||
// definition manifest-only instead of opening the wrong file.
|
||||
const resolved = resolveSource(
|
||||
src,
|
||||
null,
|
||||
buildVanillaSearchPaths(idx.sdkDir),
|
||||
).path;
|
||||
if (resolved) {
|
||||
// The recorded source file is XML (e.g. SageXml) when available:
|
||||
// jump to the precise definition inside it, not just the file.
|
||||
// jump to the precise definition inside it. If the file was modified
|
||||
// and no longer contains the id, fall back to opening the file at the
|
||||
// top rather than inventing a precise location.
|
||||
const precise = await locationInDocument(ws, resolved, def.id);
|
||||
return precise ?? new vscode.Location(vscode.Uri.file(resolved), new vscode.Position(0, 0));
|
||||
}
|
||||
@@ -305,8 +320,10 @@ export class Ra3DocumentLinkProvider implements vscode.DocumentLinkProvider {
|
||||
_token: vscode.CancellationToken,
|
||||
): Promise<vscode.DocumentLink[]> {
|
||||
if (!this.ws.isRa3Workspace()) return [];
|
||||
const idx = this.ws.index;
|
||||
const searchPaths = idx ? searchPathsFor(idx) : this.ws.searchPaths();
|
||||
const idx = this.ws.indexForDocument(document) ?? this.ws.activeIndex();
|
||||
const searchPaths = idx
|
||||
? searchPathsFor(idx)
|
||||
: this.ws.searchPaths(document);
|
||||
if (!searchPaths) return [];
|
||||
const text = document.getText();
|
||||
const doc = parseXml(text);
|
||||
|
||||
+20
-10
@@ -134,7 +134,9 @@ export async function sitesToLocations(
|
||||
|
||||
const locations: vscode.Location[] = [];
|
||||
for (const [file, fileSites] of byFile) {
|
||||
const parsed = await ws.indexer?.readDom(file);
|
||||
const parsed = await (ws.indexerForFile(file) ?? ws.activeIndexer())?.readDom(
|
||||
file,
|
||||
);
|
||||
const lineMap = parsed?.lineMap ?? null;
|
||||
for (const site of fileSites) {
|
||||
if (lineMap) {
|
||||
@@ -179,7 +181,7 @@ export async function findReferenceLocations(
|
||||
position: vscode.Position,
|
||||
): Promise<vscode.Location[] | null> {
|
||||
if (!ws.isRa3Workspace()) return null;
|
||||
scheduleRebuildIfRecordsDesync(ws, document);
|
||||
scheduleRebuildIfRecordsDesync(ws.recordsSyncSurfaceFor(document), document);
|
||||
const scope = await ws.getScope(document);
|
||||
const idx = scope.merged;
|
||||
if (!idx) return null;
|
||||
@@ -207,16 +209,24 @@ export async function showReferencesForDef(
|
||||
ws: ModWorkspace,
|
||||
args: ShowReferencesArgs,
|
||||
): Promise<void> {
|
||||
const idx = ws.index;
|
||||
const doc = vscode.workspace.textDocuments.find(
|
||||
(d) => d.uri.toString() === args.uri.toString(),
|
||||
);
|
||||
if (!doc) return;
|
||||
let idx: ModIndex | null = null;
|
||||
try {
|
||||
idx = (await ws.getCodeLensScope(doc)).merged;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!idx) return;
|
||||
const def: AssetDef = {
|
||||
type: args.type,
|
||||
// Same definition union as the lens count / Find All References.
|
||||
const defs = definitionsForReference(idx, {
|
||||
id: args.id,
|
||||
file: args.file,
|
||||
line: args.line,
|
||||
origin: "project",
|
||||
};
|
||||
const sites = referenceSitesForDef(idx, def);
|
||||
refType: null,
|
||||
selfType: null,
|
||||
});
|
||||
const sites = collectReferenceSites(idx, defs);
|
||||
const locations = await sitesToLocations(ws, sites);
|
||||
await vscode.commands.executeCommand(
|
||||
"editor.action.showReferences",
|
||||
|
||||
@@ -23,13 +23,13 @@ export async function findUnreferencedAssets(
|
||||
ws: ModWorkspace,
|
||||
args?: { type?: string },
|
||||
): Promise<void> {
|
||||
if (!ws.isRa3Workspace() || !ws.index) {
|
||||
const idx = ws.activeIndex();
|
||||
if (!ws.isRa3Workspace() || !idx) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"RA3 Mod XML: no index available yet.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const idx = ws.index;
|
||||
const byType = unreferencedByType(idx);
|
||||
|
||||
let type = args?.type;
|
||||
|
||||
Reference in New Issue
Block a user