Wire IDE symbol navigation panel
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-16 20:48:31 +03:00
parent 1fe213c5ee
commit 523a756f5c
3 changed files with 89 additions and 48 deletions
@@ -106,7 +106,7 @@ const checks = [
{
name: "module mode",
url: `${baseUrl}/editor?lang=ru&project=${projectId}&mode=module&routine=${routine}`,
mustInclude: ["data-ide-workspace", "data-left-navigation-panel", "data-right-context-inspector", "data-open-objects-bar", "data-open-document-pin", "data-open-document-close", "data-fallback-tree-search", "data-fallback-tree-filters", "Alt+1 Alt+2 Alt+3", "Редактор BSL", "Код модуля не загружен", "Выберите реальный модуль", "Основная конфигурация", "Расширение: <Имя>", "SFERA", "Среды"]
mustInclude: ["data-ide-workspace", "data-active-mode=\"module\"", "data-left-navigation-panel", "data-right-context-inspector", "data-open-objects-bar", "data-open-document-pin", "data-open-document-close", "data-fallback-tree-search", "data-fallback-tree-filters", "data-fast-bsl-editor", "data-symbol-navigation-panel", "symbol-search-input", "Alt+1 Alt+2 Alt+3", "Основная конфигурация", "SFERA", "Среды"]
},
{
name: "form mode",
@@ -186,16 +186,13 @@ try {
await restoredModuleTabs.first().click();
}
await page.locator("[data-fast-bsl-editor]").waitFor({ state: "visible", timeout: 15000 });
const symbolNavigationPanel = page.locator("[data-symbol-navigation-panel]");
if ((await symbolNavigationPanel.count()) > 0) {
await symbolNavigationPanel.waitFor({ state: "visible", timeout: 15000 });
}
await page.locator("[data-symbol-navigation-panel]").waitFor({ state: "visible", timeout: 15000 });
const symbolSearchInput = page.locator("#symbol-search-input");
if ((await symbolSearchInput.count()) > 0) {
await symbolSearchInput.fill("Проверить");
await symbolSearchInput.press("Enter");
await page.locator("[data-symbol-result]").first().waitFor({ state: "visible", timeout: 15000 });
}
await symbolSearchInput.fill("demo");
await symbolSearchInput.press("Enter");
await page.locator("[data-symbol-result]").first().waitFor({ state: "visible", timeout: 15000 });
await page.locator('button[data-editor-action="symbol-definition-row"]').first().click();
await page.locator("[data-symbol-definition]").waitFor({ state: "visible", timeout: 15000 });
const findUsagesButton = page.locator('button[data-editor-action="find-usages"]');
if ((await findUsagesButton.count()) > 0 && await findUsagesButton.isEnabled()) {
await findUsagesButton.click();
@@ -2255,6 +2255,19 @@ function EditorPanel({
value={monacoValue}
/>
</div>
<SymbolNavigationPanel
definition={symbolDefinition}
language={language}
message={symbolMessage}
onDefinition={handleDefinition}
onQueryChange={setSymbolQuery}
onReferences={handleReferences}
onSearch={handleSymbolSearch}
query={symbolQuery}
references={symbolReferences}
results={symbolResults}
state={symbolState}
/>
</section>
);
}
@@ -2262,63 +2275,94 @@ function EditorPanel({
function SymbolNavigationPanel({
definition,
language,
message,
onDefinition,
onQueryChange,
onReferences,
onSearch,
query,
references,
results
results,
state
}: Readonly<{
definition: SymbolResult | null;
language: UiLanguage;
message: string;
onDefinition: (lineageId?: string) => void;
onQueryChange: (value: string) => void;
onReferences: (lineageId?: string) => void;
onSearch: () => void;
query: string;
references: SymbolReferences | null;
results: SymbolResult[];
state: "idle" | "loading" | "error";
}>) {
const t = messages[language];
const referenceRows = references?.references.slice(0, 8) ?? [];
return (
<div className="border-t border-border p-3" data-symbol-navigation-panel>
<div className="flex items-center justify-between gap-2">
<div className="text-xs font-semibold uppercase text-muted-foreground">{t.referencesPanel}</div>
<Badge tone="neutral">{referenceRows.length}</Badge>
<div className="max-h-56 shrink-0 overflow-auto border-t border-border bg-card p-3" data-symbol-navigation-panel>
<div className="flex flex-wrap items-center gap-2">
<label className="text-xs font-semibold uppercase text-muted-foreground" htmlFor="symbol-search-input">{t.referencesPanel}</label>
<input
className="h-8 min-w-48 flex-1 border border-border bg-background px-2 text-xs outline-none focus:border-primary"
id="symbol-search-input"
onChange={(event) => onQueryChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
onSearch();
}
}}
placeholder={language === "ru" ? "Найти символ" : "Find symbol"}
value={query}
/>
<ActionButton actionId="symbol-search-panel" disabled={state === "loading"} label={state === "loading" ? t.loading : t.search} onClick={onSearch} />
<Badge tone={state === "error" ? "danger" : referenceRows.length > 0 ? "success" : "neutral"}>{referenceRows.length}</Badge>
</div>
<div className="mt-2 space-y-2">
{results.length === 0 ? (
<div className="text-xs text-muted-foreground">{language === "ru" ? "Выполните поиск символа или откройте использования выбранного объекта." : "Search a symbol or open references for the selected object."}</div>
) : (
results.map((result) => (
<div className="border border-border bg-background p-2" data-symbol-result key={result.node.lineage_id}>
<div className="truncate text-xs font-medium">{result.node.qualified_name}</div>
<div className="mt-1 truncate text-[11px] text-muted-foreground">{result.node.kind} · {formatSourceLocation(result.source, language)}</div>
<div className="mt-2 flex gap-2">
<button className="border border-border px-2 py-1 text-[11px] hover:bg-muted" data-editor-action="symbol-definition-row" onClick={() => onDefinition(result.node.lineage_id)} type="button">
{language === "ru" ? "Определение" : "Definition"}
</button>
<button className="border border-border px-2 py-1 text-[11px] hover:bg-muted" data-editor-action="symbol-references-row" onClick={() => onReferences(result.node.lineage_id)} type="button">
{t.findUsages}
</button>
{message ? (
<div className={["mt-2 truncate text-xs", state === "error" ? "text-destructive" : "text-muted-foreground"].join(" ")} data-symbol-message>{message}</div>
) : null}
<div className="mt-3 grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(280px,0.8fr)]">
<div className="min-w-0 space-y-2">
{results.length === 0 ? (
<div className="text-xs text-muted-foreground">{language === "ru" ? "Выполните поиск символа или откройте использования выбранного объекта." : "Search a symbol or open references for the selected object."}</div>
) : (
results.map((result) => (
<div className="border border-border bg-background p-2" data-symbol-result key={result.node.lineage_id}>
<div className="truncate text-xs font-medium">{result.node.qualified_name}</div>
<div className="mt-1 truncate text-[11px] text-muted-foreground">{result.node.kind} · {formatSourceLocation(result.source, language)}</div>
<div className="mt-2 flex gap-2">
<button className="border border-border px-2 py-1 text-[11px] hover:bg-muted" data-editor-action="symbol-definition-row" onClick={() => onDefinition(result.node.lineage_id)} type="button">
{language === "ru" ? "Определение" : "Definition"}
</button>
<button className="border border-border px-2 py-1 text-[11px] hover:bg-muted" data-editor-action="symbol-references-row" onClick={() => onReferences(result.node.lineage_id)} type="button">
{t.findUsages}
</button>
</div>
</div>
))
)}
</div>
<div className="min-w-0 space-y-2">
{definition ? (
<div className="border border-primary/30 bg-primary/5 p-2 text-xs" data-symbol-definition>
<div className="font-medium">{definition.node.qualified_name}</div>
<div className="mt-1 text-muted-foreground">{formatSourceLocation(definition.source, language)}</div>
</div>
))
)}
) : null}
{referenceRows.length > 0 ? (
<div className="space-y-1" data-symbol-references>
{referenceRows.map((reference) => (
<div className="border border-border bg-muted/30 p-2 text-xs" key={reference.edge_id}>
<div className="truncate font-medium">{reference.source?.qualified_name ?? reference.target?.qualified_name ?? reference.kind}</div>
<div className="mt-1 truncate text-muted-foreground">{reference.kind} · {reference.direction} · {formatSourceLocation(reference.location, language)}</div>
</div>
))}
</div>
) : null}
</div>
</div>
{definition ? (
<div className="mt-3 border border-primary/30 bg-primary/5 p-2 text-xs" data-symbol-definition>
<div className="font-medium">{definition.node.qualified_name}</div>
<div className="mt-1 text-muted-foreground">{formatSourceLocation(definition.source, language)}</div>
</div>
) : null}
{referenceRows.length > 0 ? (
<div className="mt-3 space-y-1" data-symbol-references>
{referenceRows.map((reference) => (
<div className="border border-border bg-muted/30 p-2 text-xs" key={reference.edge_id}>
<div className="truncate font-medium">{reference.source?.qualified_name ?? reference.target?.qualified_name ?? reference.kind}</div>
<div className="mt-1 truncate text-muted-foreground">{reference.kind} · {reference.direction} · {formatSourceLocation(reference.location, language)}</div>
</div>
))}
</div>
) : null}
</div>
);
}