From 76d33f030650431f33b525019ad3f04703986c67 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:18:50 +0800 Subject: [PATCH] feat(ui): IDEA-style message dialogs, project widget popup and Chinese UI text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Message dialogs: `Dialog` gets an `icon` (question / info / warning / error) that renders IDEA's alert layout: no title bar, a 32px icon, bold title, text, checkbox under it. Every confirmation helper (prompt, VCS, services, project tree, keymap, LSP choices, run, database, Replace All) uses it. Open Project matches IDEA: "This Window" is the default and has focus. The Code Analysis and TODO commit checks default to Review. Project widget: the title bar project button opens a popup like IDEA's ProjectWidget, with Open… / Clone Repository…, the projects open in other windows (new `window_open_projects` command) and recent projects, filtered by typing. Chinese UI: UI text stays written in English and is translated through a source-text catalog (`locales/text/zh.json`, about 2300 strings). The UI primitives (buttons, checkboxes, selects, tabs, dialogs, menus, tooltips, toasts, settings labels and hints) translate the strings they are given. Action titles and categories are localised in the registry, and static JSX text uses ``, so text follows a locale switch without a reload. The Settings window now syncs the locale too. Settings search matches both languages. Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/window.rs | 30 + src/bindings.ts | 9 + src/editor/core/view/FindBar.tsx | 56 +- src/editor/core/view/NavBarPopup.tsx | 60 +- src/editor/lsp/Popups.module.css | 8 +- src/editor/lsp/Popups.test.tsx | 4 +- src/editor/lsp/Popups.tsx | 84 +- src/editor/lsp/popupStore.ts | 2 + src/editor/refactor/safeDelete.ts | 43 +- src/features/analysis/AnalysisDialogs.tsx | 22 +- src/features/analysis/dialogs.tsx | 8 +- src/features/app/InvalidateCaches.tsx | 21 +- src/features/app/__tests__/closeGuard.test.ts | 2 +- src/features/app/app.module.css | 7 - src/features/app/closeGuard.ts | 1 + src/features/app/index.ts | 3 + src/features/app/prompt.tsx | 18 +- src/features/app/windows.ts | 8 +- src/features/build-tools/BuildToolView.tsx | 110 +- src/features/build-tools/ExecutePopup.tsx | 15 +- src/features/build/BuildView.tsx | 30 +- src/features/code-insight/HierarchyView.tsx | 21 +- src/features/code-insight/ProblemsView.tsx | 40 +- src/features/code-insight/ServerStatus.tsx | 8 +- src/features/code-insight/UsagesView.tsx | 21 +- src/features/coverage/CoverageView.tsx | 24 +- src/features/database/DatabaseView.tsx | 88 +- src/features/database/Dialogs.tsx | 151 +- src/features/database/ObjectDialogs.tsx | 124 +- src/features/database/ResultsView.tsx | 36 +- src/features/database/TableEditor.tsx | 23 +- src/features/debugger/ui/DebugToolWindow.tsx | 5 +- src/features/debugger/ui/Overlays.tsx | 121 +- src/features/github/PullRequestsView.tsx | 125 +- src/features/github/dialogs.tsx | 72 +- src/features/http-client/HttpClientView.tsx | 10 +- src/features/http-client/editor.tsx | 22 +- src/features/index.ts | 3 +- src/features/perf/MemoryStatus.tsx | 8 +- src/features/perf/PerfOverlay.tsx | 10 +- src/features/plugins/PluginsPage.tsx | 69 +- src/features/profiler/ProfilerView.tsx | 17 +- src/features/project-tree/ConfirmDialog.tsx | 6 +- .../project-tree/FileRefactorDialogs.tsx | 8 +- .../project-tree/ProjectView.module.css | 7 - src/features/run/ConfigChooser.tsx | 18 +- src/features/run/EditConfigsDialog.tsx | 54 +- src/features/run/RunView.tsx | 25 +- src/features/run/dialogs.tsx | 12 +- src/features/run/stackTrace.tsx | 4 +- src/features/scratch/ScratchChooser.tsx | 12 +- .../search/everywhere/SearchEverywhere.tsx | 34 +- .../search/find/FindInFiles.module.css | 2 - src/features/search/find/FindInFiles.tsx | 156 +- .../search/find/FindToolWindow.module.css | 1 - src/features/search/find/FindToolWindow.tsx | 72 +- src/features/search/find/parts.tsx | 18 +- src/features/search/recent/RecentPopups.tsx | 58 +- .../search/structure/StructureView.tsx | 37 +- src/features/services/Services.module.css | 6 - src/features/services/ServicesView.tsx | 114 +- src/features/services/dialogs.tsx | 40 +- src/features/settings/components/Form.tsx | 20 +- .../settings/dialog/SettingsDialog.tsx | 30 +- .../settings/keymap/Keymap.module.css | 4 - .../settings/keymap/KeymapDialogs.tsx | 15 +- src/features/settings/keymap/KeymapPage.tsx | 7 +- .../settings/keymap/RecordShortcutDialog.tsx | 39 +- .../languages/AddInterpreterDialog.tsx | 19 +- .../languages/ProjectStructurePage.tsx | 33 +- .../languages/PythonInterpreterPage.tsx | 32 +- src/features/settings/model/pages.ts | 6 +- src/features/settings/pages/behavior.tsx | 23 +- .../settings/pages/editor/codeStyle.tsx | 12 +- .../settings/pages/editor/colorScheme.tsx | 8 +- src/features/settings/pages/editor/font.tsx | 6 +- src/features/settings/pages/execution.tsx | 7 +- src/features/settings/pages/tools.tsx | 31 +- src/features/settings/pages/vcs.tsx | 20 +- .../templates/settings/FileTemplatesPage.tsx | 28 +- .../templates/settings/LiveTemplatesPage.tsx | 40 +- .../templates/settings/PostfixPage.tsx | 7 +- src/features/todo/TodoSettingsPage.tsx | 8 +- src/features/todo/TodoView.tsx | 9 +- src/features/tools/LanguageServersPage.tsx | 48 +- src/features/vcs/CommitView.tsx | 49 +- src/features/vcs/CompareBranches.tsx | 32 +- src/features/vcs/GitLog.tsx | 76 +- src/features/vcs/LocalHistory.tsx | 37 +- src/features/vcs/MergeEditor.tsx | 38 +- src/features/vcs/changelistsUi.tsx | 14 +- src/features/vcs/clone.tsx | 29 +- src/features/vcs/commitChecks.tsx | 52 +- src/features/vcs/common.module.css | 6 - src/features/vcs/confirmations.tsx | 25 +- src/features/vcs/conflictsUi.tsx | 12 +- src/features/vcs/dialogs.tsx | 5 +- src/features/vcs/operations.tsx | 96 +- src/features/vcs/patches.tsx | 51 +- src/features/vcs/rebase.tsx | 21 +- src/features/vcs/remoteUi.tsx | 38 +- src/features/vcs/shelf.tsx | 34 +- src/features/viewers/HexViewer.tsx | 8 +- src/features/viewers/ImageViewer.tsx | 4 +- src/features/welcome/CustomizePage.tsx | 6 +- src/features/welcome/ProjectPopup.module.css | 111 + src/features/welcome/ProjectPopup.tsx | 209 ++ src/features/welcome/ProjectsPage.tsx | 25 +- src/features/welcome/RecentProjectsPopup.tsx | 6 +- src/features/welcome/WelcomeView.tsx | 8 +- src/features/welcome/index.ts | 4 +- src/platform/actions.ts | 49 +- src/platform/i18n.test.ts | 18 + src/platform/i18n.ts | 53 +- src/platform/locales/en.json | 11 +- src/platform/locales/text/zh.json | 2285 +++++++++++++++++ src/platform/locales/zh.json | 11 +- src/ui/Button.tsx | 10 +- src/ui/Checkbox.tsx | 6 +- src/ui/Dialog.module.css | 59 + src/ui/Dialog.tsx | 119 +- src/ui/Feedback.tsx | 9 +- src/ui/Input.tsx | 8 +- src/ui/Menu.tsx | 16 +- src/ui/Select.tsx | 12 +- src/ui/T.tsx | 6 + src/ui/Tabs.tsx | 8 +- src/ui/Tooltip.tsx | 3 +- src/ui/Tree.tsx | 3 +- src/ui/index.ts | 1 + src/workbench/ErrorBoundary.tsx | 4 +- src/workbench/Notifications.tsx | 17 +- src/workbench/StatusBar.tsx | 14 +- src/workbench/TitleBar.tsx | 21 +- src/workbench/Welcome.tsx | 12 +- src/workbench/contributions.ts | 13 +- src/workbench/toolwindows/ToolWindows.tsx | 53 +- 138 files changed, 5507 insertions(+), 986 deletions(-) create mode 100644 src/features/welcome/ProjectPopup.module.css create mode 100644 src/features/welcome/ProjectPopup.tsx create mode 100644 src/platform/locales/text/zh.json create mode 100644 src/ui/T.tsx diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 235fdf9cb..e56caeb2f 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -227,6 +227,7 @@ pub fn builder() -> tauri_specta::Builder { window::window_toggle_fullscreen, window::window_take_launch, window::window_close_context, + window::window_open_projects, window::app_quit, window::app_cancel_quit, window::menu_configure, diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index dcc49989f..0718fe643 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -59,6 +59,36 @@ pub fn window_take_launch(window: WebviewWindow, windows: State<'_, WindowsState windows.take_launch(window.label()) } +#[derive(Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct OpenProject { + pub name: String, + pub root: PathBuf, + /// Open in the calling window. + pub current: bool, +} + +/// Projects open in project windows, in window order (title bar project widget). +#[tauri::command] +#[specta::specta] +pub fn window_open_projects( + app: AppHandle, + window: WebviewWindow, + state: State<'_, AppState>, +) -> IdeResult> { + Ok(windows::project_window_labels(&app) + .into_iter() + .filter_map(|label| { + let project = state.project(&label).ok()?; + Some(OpenProject { + name: project.root.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(), + root: project.root.clone(), + current: label == window.label(), + }) + }) + .collect()) +} + #[derive(Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct CloseContext { diff --git a/src/bindings.ts b/src/bindings.ts index 119fb7d66..076cde334 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -434,6 +434,8 @@ export const commands = { settingsPage: string | null, } | null>("window_take_launch"), windowCloseContext: () => __TAURI_INVOKE("window_close_context"), + /** Projects open in project windows, in window order (title bar project widget). */ + windowOpenProjects: () => typedError(__TAURI_INVOKE("window_open_projects")), /** Quits: closes every window, each of which runs its own unsaved-changes check. */ appQuit: () => __TAURI_INVOKE("app_quit"), /** A window vetoed the quit (user cancelled its unsaved-changes prompt). */ @@ -2716,6 +2718,13 @@ export type OpenOutcome = /** The caller should open the project in its own window. */ { kind: "current" }; +export type OpenProject = { + name: string, + root: string, + /** Open in the calling window. */ + current: boolean, +}; + /** A node of a document outline (Structure tool window, breadcrumbs). */ export type OutlineItem = { name: string, diff --git a/src/editor/core/view/FindBar.tsx b/src/editor/core/view/FindBar.tsx index 985d4e891..8ce10a071 100644 --- a/src/editor/core/view/FindBar.tsx +++ b/src/editor/core/view/FindBar.tsx @@ -8,7 +8,7 @@ import { useEffect, useLayoutEffect, useRef, type KeyboardEvent } from "react"; import { create } from "zustand"; import clsx from "clsx"; import type { EditorView } from "@codemirror/view"; -import { Button, IconButton, SearchField } from "@/ui"; +import { Button, IconButton, SearchField, T } from "@/ui"; import { editorController, useFindBars, useTabs } from "../controller"; import { countMatches, @@ -198,7 +198,11 @@ function FindBarInner({ paneId, replace, seq }: { paneId: string; replace: boole else run(replaceNext); } else run(back ? findPrevious : findNext); } else if (e.altKey && !e.metaKey && !e.ctrlKey) { - const map: Record = { KeyC: "caseSensitive", KeyW: "wholeWord", KeyX: "regexp" }; + const map: Record = { + KeyC: "caseSensitive", + KeyW: "wholeWord", + KeyX: "regexp", + }; const k = map[e.code]; if (k) { e.preventDefault(); @@ -235,17 +239,51 @@ function FindBarInner({ paneId, replace, seq }: { paneId: string; replace: boole invalid={invalid || none} onKeyDown={(e) => onKeyDown(e, "find")} toggles={[ - { id: "case", icon: "case", title: "Match Case", shortcut: "⌥C", pressed: opts.caseSensitive, onToggle: () => toggle("caseSensitive") }, - { id: "word", icon: "word", title: "Words", shortcut: "⌥W", pressed: opts.wholeWord, onToggle: () => toggle("wholeWord") }, - { id: "regex", icon: "regex", title: "Regex", shortcut: "⌥X", pressed: opts.regexp, onToggle: () => toggle("regexp") }, - { id: "scope", icon: "filter", title: "In Selection", shortcut: "⌥E", pressed: scoped, onToggle: () => toggleScope(paneId) }, + { + id: "case", + icon: "case", + title: "Match Case", + shortcut: "⌥C", + pressed: opts.caseSensitive, + onToggle: () => toggle("caseSensitive"), + }, + { + id: "word", + icon: "word", + title: "Words", + shortcut: "⌥W", + pressed: opts.wholeWord, + onToggle: () => toggle("wholeWord"), + }, + { + id: "regex", + icon: "regex", + title: "Regex", + shortcut: "⌥X", + pressed: opts.regexp, + onToggle: () => toggle("regexp"), + }, + { + id: "scope", + icon: "filter", + title: "In Selection", + shortcut: "⌥E", + pressed: scoped, + onToggle: () => toggleScope(paneId), + }, ]} />
{counter} - run(findPrevious)} /> + run(findPrevious)} + /> run(findNext)} />
diff --git a/src/editor/core/view/NavBarPopup.tsx b/src/editor/core/view/NavBarPopup.tsx index a08a6fe0e..a196795a8 100644 --- a/src/editor/core/view/NavBarPopup.tsx +++ b/src/editor/core/view/NavBarPopup.tsx @@ -9,11 +9,19 @@ import clsx from "clsx"; import { create } from "zustand"; import { call, commands, isTauri, type OutlineItem } from "@/platform/ipc"; import { useProject } from "@/platform/project"; -import { Icon, Popup } from "@/ui"; +import { Icon, Popup, T } from "@/ui"; import { editorController, useEditorStatus } from "../controller"; import { useOutline } from "../model/document"; import { symbolChain } from "./outline"; -import { dirChildren, iconOf, navSegments, stepSegment, symbolChildren, type NavChild, type NavSegment } from "./navBar"; +import { + dirChildren, + iconOf, + navSegments, + stepSegment, + symbolChildren, + type NavChild, + type NavSegment, +} from "./navBar"; import styles from "./NavBarPopup.module.css"; export const useNavBarPopup = create<{ open: boolean; seq: number }>(() => ({ open: false, seq: 0 })); @@ -48,7 +56,12 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number } const project = useProject((s) => s.project); const outline = useOutline((s) => s[path]); const initial = useMemo( - () => navSegments(project ? { root: project.root, name: project.name } : null, path, outline ? symbolChain(outline, line, col) : []), + () => + navSegments( + project ? { root: project.root, name: project.name } : null, + path, + outline ? symbolChain(outline, line, col) : [], + ), // The bar is a snapshot of where ⌘↑ was pressed. // eslint-disable-next-line react-hooks/exhaustive-deps [], @@ -76,7 +89,10 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number } const choose = (child: NavChild) => { if (!list) return; if (child.kind === "dir") { - const segs: NavSegment[] = [...segments.slice(0, list.seg + 1), { kind: "dir", path: child.path, label: child.label }]; + const segs: NavSegment[] = [ + ...segments.slice(0, list.seg + 1), + { kind: "dir", path: child.path, label: child.label }, + ]; setSegments(segs); setActive(segs.length - 1); void openList(segs.length - 1, segs); @@ -101,7 +117,8 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number } if (e.key === "ArrowLeft") move(-1); else if (e.key === "ArrowRight") move(1); else if (e.key === "ArrowDown" && !list) void openList(active); - else if (e.key === "ArrowDown" && list) setList({ ...list, active: Math.min(list.items.length - 1, list.active + 1) }); + else if (e.key === "ArrowDown" && list) + setList({ ...list, active: Math.min(list.items.length - 1, list.active + 1) }); else if (e.key === "ArrowUp" && list) setList({ ...list, active: Math.max(0, list.active - 1) }); else if (e.key === "Enter" && list) { const child = list.items[list.active]; @@ -123,8 +140,23 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number } }, [list]); return ( - -
+ +
{segments.map((s, i) => ( @@ -147,8 +179,18 @@ function NavBar({ path, line, col }: { path: string; line: number; col: number } ))}
{list && ( -
- {list.items.length === 0 &&
Nothing here
} +
+ {list.items.length === 0 && ( +
+ Nothing here +
+ )} {list.items.map((c, i) => (
{ expect(useLspPopup.getState().popup).toBeNull(); }); - it("Windows: View Usages · Delete Anyway · Cancel; Escape and the close button cancel", () => { + it("Windows: View Usages · Delete Anyway · Cancel; Escape and Cancel cancel", () => { usePlatform.setState({ os: "windows" }); render(); let { cancel } = openUsagesDetected(); @@ -53,7 +53,7 @@ describe("Usages Detected dialog", () => { fireEvent.keyDown(document.body, { key: "Escape" }); expect(cancel).toHaveBeenCalledTimes(1); ({ cancel } = openUsagesDetected()); - fireEvent.click(screen.getByRole("button", { name: "Close" })); + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); expect(cancel).toHaveBeenCalledTimes(1); expect(screen.queryByRole("dialog")).toBeNull(); }); diff --git a/src/editor/lsp/Popups.tsx b/src/editor/lsp/Popups.tsx index 7d7540205..3bd714f1f 100644 --- a/src/editor/lsp/Popups.tsx +++ b/src/editor/lsp/Popups.tsx @@ -1,7 +1,19 @@ /** React popups for LSP editor features, rendered as a workbench overlay. */ import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; -import { Button, Dialog, HighlightedText, Icon, Input, List, Popup, SymbolBadge, fileIconName, fuzzyMatch } from "@/ui"; +import { + Button, + Dialog, + HighlightedText, + Icon, + Input, + List, + Popup, + SymbolBadge, + fileIconName, + fuzzyMatch, + T, +} from "@/ui"; import { basename, relativePath, useProject } from "@/platform/project"; import { editorService } from "@/workbench/contributions"; import { closePopup, useLspPopup, type ListPopupItem, type LspPopup } from "./popupStore"; @@ -24,9 +36,7 @@ function ListPopup({ popup }: { popup: Extract }) { const [query, setQuery] = useState(""); const filtered = useMemo(() => { if (!query) return popup.items.map((item) => ({ item, match: null as number[] | null })); - return popup.items - .map((item) => ({ item, match: fuzzyMatch(item.label, query) })) - .filter((r) => r.match !== null); + return popup.items.map((item) => ({ item, match: fuzzyMatch(item.label, query) })).filter((r) => r.match !== null); }, [popup.items, query]); const firstEnabled = Math.max( 0, @@ -59,7 +69,9 @@ function ListPopup({ popup }: { popup: Extract }) { const width = useMemo(() => { const chars = Math.max( popup.title.length, - ...popup.items.map((i) => i.label.length + (i.preview?.length ?? 0) + (i.detail?.length ?? 0) + (i.shortcut?.length ?? 0)), + ...popup.items.map( + (i) => i.label.length + (i.preview?.length ?? 0) + (i.detail?.length ?? 0) + (i.shortcut?.length ?? 0), + ), ); return Math.min(720, Math.max(320, chars * 7 + 90)); }, [popup]); @@ -85,7 +97,11 @@ function ListPopup({ popup }: { popup: Extract }) { hoverSelects aria-label={popup.title} onActivate={(r) => activate(r.item)} - empty={Nothing matches} + empty={ + + Nothing matches + + } renderItem={({ item, match }) => (
} return true; }; return ( - +
{ e.preventDefault(); @@ -143,7 +166,8 @@ function RenamePopup({ popup }: { popup: Extract } value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={(e) => { - if ((e.key === "ArrowDown" || e.key === "ArrowUp") && pick(e.key === "ArrowDown" ? 1 : -1)) e.preventDefault(); + if ((e.key === "ArrowDown" || e.key === "ArrowUp") && pick(e.key === "ArrowDown" ? 1 : -1)) + e.preventDefault(); }} invalid={!value.trim()} aria-label="New name" @@ -214,14 +238,11 @@ function DefinitionPopup({ popup }: { popup: Extract
- renderCode(popup.text, popup.language, "cm-lsp-code")} - /> + renderCode(popup.text, popup.language, "cm-lsp-code")} />
); @@ -276,23 +297,23 @@ function PreviewDialog({ popup }: { popup: Extract ) : ( -
    - {summary.entries.map((e) => ( -
  • - - {basename(e.path)} - {relativePath(root, e.path)} - - {e.operation === "rename" && e.to && → {basename(e.to)}} - {e.operation && e.operation !== "rename" && {e.operation}} - {e.edits > 0 && ( - - {e.edits} {e.edits === 1 ? "occurrence" : "occurrences"} - - )} -
  • - ))} -
+
    + {summary.entries.map((e) => ( +
  • + + {basename(e.path)} + {relativePath(root, e.path)} + + {e.operation === "rename" && e.to && → {basename(e.to)}} + {e.operation && e.operation !== "rename" && {e.operation}} + {e.edits > 0 && ( + + {e.edits} {e.edits === 1 ? "occurrence" : "occurrences"} + + )} +
  • + ))} +
)} ); @@ -309,6 +330,7 @@ function ChoiceDialog({ popup }: { popup: Extract open onClose={() => answer(popup.onDismiss)} title={popup.title} + icon={popup.icon ?? "question"} width={460} buttons={popup.choices.map((c) => ({ label: c.label, @@ -317,7 +339,7 @@ function ChoiceDialog({ popup }: { popup: Extract onClick: () => answer(c.run), }))} > -
{popup.message}
+ {popup.message} {popup.detail &&
{popup.detail}
} ); diff --git a/src/editor/lsp/popupStore.ts b/src/editor/lsp/popupStore.ts index ba582d328..f402c6967 100644 --- a/src/editor/lsp/popupStore.ts +++ b/src/editor/lsp/popupStore.ts @@ -68,6 +68,8 @@ export type LspPopup = title: string; message: string; detail?: string; + /** Defaults to `question`. */ + icon?: "question" | "warning" | "error" | "info"; choices: PopupChoice[]; /** Escape / close button / replaced by another popup. */ onDismiss?: () => void; diff --git a/src/editor/refactor/safeDelete.ts b/src/editor/refactor/safeDelete.ts index cdc955abf..e55af3e57 100644 --- a/src/editor/refactor/safeDelete.ts +++ b/src/editor/refactor/safeDelete.ts @@ -56,7 +56,9 @@ export function deletionRange( const restOk = !rest || /^[;,]$/.test(rest) || - (!!comments.line && (rest.startsWith(comments.line) || /^[;,]\s*/.test(rest) && rest.replace(/^[;,]\s*/, "").startsWith(comments.line))); + (!!comments.line && + (rest.startsWith(comments.line) || + (/^[;,]\s*/.test(rest) && rest.replace(/^[;,]\s*/, "").startsWith(comments.line)))); if (!restOk) return null; // Attached comments and annotations above (no blank line in between). @@ -88,7 +90,8 @@ export function deletionRange( const next = last.number + 1; if (blank(next) && next < doc.lines) end = doc.line(next).to + 1; else if (blank(next)) end = doc.line(next).to; - else if (blank(startLine - 1) && /^[}\])]|^end\b/.test(doc.line(next).text.trim())) start = doc.line(startLine - 1).from; + else if (blank(startLine - 1) && /^[}\])]|^end\b/.test(doc.line(next).text.trim())) + start = doc.line(startLine - 1).from; } else if (startLine > 1) { // Last line: take the line break before it (and a blank line above). start = doc.line(blank(startLine - 1) && startLine > 2 ? startLine - 2 : startLine - 1).to; @@ -147,14 +150,17 @@ const FUNCTION_STATEMENT = /function|method|^def|^fn|func|procedure/; /** Declaration statement of a syntax-resolved local, or why it can't be deleted on its own. */ export function localDeclaration(doc: Text, symbol: LocalSymbol, statement: SyntaxNode | null): Range | string { if (symbol.kind === "parameter") return "Deleting a parameter changes its call sites; use Change Signature"; - if (symbol.kind === "label" || symbol.kind === "type" || symbol.kind === "macro") return "Caret should be on a variable or function declaration"; + if (symbol.kind === "label" || symbol.kind === "type" || symbol.kind === "macro") + return "Caret should be on a variable or function declaration"; if (!symbol.certain) return `'${symbol.name}' is declared more than once in this scope`; - if (!statement || !rangeContains(statement.range, symbol.definition.start)) return "The declaration could not be determined"; + if (!statement || !rangeContains(statement.range, symbol.definition.start)) + return "The declaration could not be determined"; const kind = statement.kind; if (symbol.kind === "function") { return FUNCTION_STATEMENT.test(kind) ? statement.range : "The declaration could not be determined"; } - if (COMPOUND_STATEMENT.test(kind) || !DECLARATION_STATEMENT.test(kind)) return `'${symbol.name}' is not declared by a separate statement`; + if (COMPOUND_STATEMENT.test(kind) || !DECLARATION_STATEMENT.test(kind)) + return `'${symbol.name}' is not declared by a separate statement`; const { from, to } = rangeToOffsets(doc, statement.range); const defEnd = rangeToOffsets(doc, symbol.definition).to; // Up to the initializer, the statement must declare only this name (no `a, b` / destructuring). @@ -193,7 +199,13 @@ async function resolveTarget(view: EditorView, ctx: EditorDocContext): Promise { const outside = (path: string, range: Range) => - path !== ctx.path || comparePos(range.start, target.range.start) < 0 || comparePos(range.start, target.range.end) > 0; - if (target.local) return target.local.references.filter((r) => outside(ctx.path, r)).map((range) => ({ path: ctx.path, range })); + path !== ctx.path || + comparePos(range.start, target.range.start) < 0 || + comparePos(range.start, target.range.end) > 0; + if (target.local) + return target.local.references.filter((r) => outside(ctx.path, r)).map((range) => ({ path: ctx.path, range })); const refs = await lspRequest(() => commands.lspReferences(ctx.doc, target.at, false)); return refs.filter((l) => outside(l.path, l.range)); } @@ -242,7 +260,11 @@ async function findUsages(ctx: EditorDocContext, target: SafeDeleteTarget): Prom export type UsagesAnswer = "view" | "delete" | "cancel"; /** `subject`: "Function 'foo'", "File 'a.ts'", "3 files" (`plural`). */ -export function usagesMessage(subject: string, usages: readonly Location[], plural = false): { message: string; detail: string } { +export function usagesMessage( + subject: string, + usages: readonly Location[], + plural = false, +): { message: string; detail: string } { const n = usages.length; const files = new Set(usages.map((u) => u.path)).size; const noun = n === 1 ? "usage that is" : "usages that are"; @@ -266,6 +288,7 @@ export function askUsagesDetected(subject: string, usages: readonly Location[], openPopup({ kind: "choice", title: "Usages Detected", + icon: "warning", message, detail, choices: [ diff --git a/src/features/analysis/AnalysisDialogs.tsx b/src/features/analysis/AnalysisDialogs.tsx index d6eaa486b..0f4fcd13d 100644 --- a/src/features/analysis/AnalysisDialogs.tsx +++ b/src/features/analysis/AnalysisDialogs.tsx @@ -9,7 +9,7 @@ import { type StructuralLanguage, } from "@/platform/ipc"; import { basename } from "@/platform/project"; -import { Checkbox, Dialog, Input, Select, Spinner } from "@/ui"; +import { Checkbox, Dialog, Input, Select, Spinner, T } from "@/ui"; import { ScopeFields, openDialog } from "./dialogs"; import { locateDuplicates, loadDuplicateOptions } from "./duplicates"; import { runInspection } from "./inspect"; @@ -45,7 +45,9 @@ function InspectCodeDialog({ onClose }: { onClose: () => void }) { >
-
Files are checked by their language servers; files without one are skipped.
+
+ Files are checked by their language servers; files without one are skipped. +
); @@ -76,7 +78,9 @@ function DuplicatesDialog({ onClose }: { onClose: () => void }) {