дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(workbench): Reset project state when a window switches projects
Opening another project in the same window left the previous project's state in module-level stores: Amend and the commit message carried over, debug sessions kept running, and Git Log, usages, hierarchy, inspection, HTTP client, recent locations, navigation history and notifications still showed the old project. Reset each of them on a project root change, stop the old debug sessions, ask about unsaved changes before leaving a project, close the settings window (it would apply to the new project) and end the indexing task when a scan is cancelled.
Цей коміт міститься в:
1 джерело
9a2025b76e
коміт
5f6e2a9c85
30 files changed
+216
-26
No files matched your search
@@ -49,14 +49,16 @@ interface DuplicatesState {
|
||||
last: { choice: ScopeChoice; options: DuplicateOptions } | null;
|
||||
}
|
||||
|
||||
export const useDuplicates = create<DuplicatesState>(() => ({
|
||||
const initialDuplicates = (): DuplicatesState => ({
|
||||
running: false,
|
||||
report: null,
|
||||
scopeLabel: null,
|
||||
error: null,
|
||||
selected: null,
|
||||
last: null,
|
||||
}));
|
||||
});
|
||||
|
||||
export const useDuplicates = create<DuplicatesState>(initialDuplicates);
|
||||
|
||||
let inflight: RequestId | null = null;
|
||||
|
||||
@@ -64,6 +66,13 @@ export function cancelDuplicates() {
|
||||
if (inflight !== null) void commands.cancelRequest(inflight).catch(() => undefined);
|
||||
}
|
||||
|
||||
/** The window switched projects: stops the run and forgets its report. */
|
||||
export function resetDuplicates() {
|
||||
cancelDuplicates();
|
||||
inflight = null;
|
||||
useDuplicates.setState(initialDuplicates());
|
||||
}
|
||||
|
||||
export async function locateDuplicates(choice: ScopeChoice, options: DuplicateOptions): Promise<void> {
|
||||
const scope = await resolveScope(choice);
|
||||
if (typeof scope === "string") {
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
* Duplicates and Search / Replace Structurally.
|
||||
*/
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { registerOverlay, registerToolWindow } from "@/workbench/contributions";
|
||||
import { showDuplicatesDialog, showInspectCodeDialog, showStructuralDialog } from "./AnalysisDialogs";
|
||||
import { AnalysisDialogHost } from "./dialogs";
|
||||
import { resetDuplicates } from "./duplicates";
|
||||
import { DuplicatesHeaderActions, DuplicatesView } from "./DuplicatesView";
|
||||
import { resetInspection } from "./inspect";
|
||||
import { InspectionHeaderActions, InspectionView } from "./InspectionView";
|
||||
|
||||
export function activate(): () => void {
|
||||
const disposers = [
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root === prev.project?.root) return;
|
||||
resetInspection();
|
||||
resetDuplicates();
|
||||
}),
|
||||
registerOverlay({ id: "analysis.dialogs", component: AnalysisDialogHost }),
|
||||
registerToolWindow({
|
||||
id: "inspection",
|
||||
|
||||
@@ -101,7 +101,7 @@ interface InspectState {
|
||||
last: ScopeChoice | null;
|
||||
}
|
||||
|
||||
export const useInspection = create<InspectState>(() => ({
|
||||
const initialInspection = (): InspectState => ({
|
||||
running: false,
|
||||
total: 0,
|
||||
done: 0,
|
||||
@@ -110,7 +110,9 @@ export const useInspection = create<InspectState>(() => ({
|
||||
scopeLabel: null,
|
||||
error: null,
|
||||
last: null,
|
||||
}));
|
||||
});
|
||||
|
||||
export const useInspection = create<InspectState>(initialInspection);
|
||||
|
||||
let inflight: RequestId | null = null;
|
||||
|
||||
@@ -119,6 +121,13 @@ export function cancelInspection() {
|
||||
void commands.cancelRequest(inflight).catch(() => undefined);
|
||||
}
|
||||
|
||||
/** The window switched projects: stops the run and forgets its results. */
|
||||
export function resetInspection() {
|
||||
cancelInspection();
|
||||
inflight = null;
|
||||
useInspection.setState(initialInspection());
|
||||
}
|
||||
|
||||
export async function runInspection(choice: ScopeChoice): Promise<void> {
|
||||
const scope = await resolveScope(choice);
|
||||
if (typeof scope === "string") {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
import { commands, isTauri, onIdeEvent, type Settings } from "@/platform/ipc";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { provideLeaveProjectGuard, useProject } from "@/platform/project";
|
||||
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
||||
import { ask, type Ask } from "./prompt";
|
||||
|
||||
@@ -115,6 +115,15 @@ async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Unsaved changes before this window opens another project in place or closes it. */
|
||||
export function confirmLeave(deps: CloseDeps = defaultDeps): Promise<boolean> {
|
||||
const s = useProject.getState();
|
||||
return decideClose(
|
||||
{ quitting: false, windows: 2, confirmExit: false, dirty: dirtyCount(), project: s.project?.name ?? null },
|
||||
deps,
|
||||
);
|
||||
}
|
||||
|
||||
/** Quit: confirm once (if enabled), then let the backend close every window. */
|
||||
export async function quitApp(deps: CloseDeps = defaultDeps): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
@@ -134,6 +143,7 @@ export function installCloseGuard(deps: CloseDeps = defaultDeps): () => void {
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) dirtyDocs.clear();
|
||||
}),
|
||||
provideLeaveProjectGuard(() => confirmLeave(deps)),
|
||||
];
|
||||
let unlisten: (() => void) | undefined;
|
||||
let disposed = false;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { Action } from "@/platform/actions";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { basename, openProject, useProject } from "@/platform/project";
|
||||
import { basename, confirmLeaveProject, openProject, useProject } from "@/platform/project";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { ask } from "./prompt";
|
||||
|
||||
@@ -24,6 +24,7 @@ function desktopOnly(): boolean {
|
||||
|
||||
async function openHere(path: string) {
|
||||
if (useProject.getState().project?.root === path) return;
|
||||
if (!(await confirmLeaveProject())) return;
|
||||
try {
|
||||
await openProject(path);
|
||||
} catch (e) {
|
||||
|
||||
@@ -45,6 +45,17 @@ export const useHierarchy = create<HierarchyState>(() => ({
|
||||
generation: 0,
|
||||
}));
|
||||
|
||||
/** The window switched projects: the tree refers to the previous one's documents. */
|
||||
export function resetHierarchy() {
|
||||
useHierarchy.setState((s) => ({
|
||||
root: null,
|
||||
children: new Map(),
|
||||
failed: new Set(),
|
||||
loading: false,
|
||||
generation: s.generation + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
export function directionLabel(mode: HierarchyMode, direction: HierarchyDirection): string {
|
||||
if (mode === "call") return direction === "incoming" ? "Callers" : "Callees";
|
||||
return direction === "incoming" ? "Supertypes" : "Subtypes";
|
||||
|
||||
@@ -11,9 +11,9 @@ import { installProblemDecorations } from "./decorations";
|
||||
import { ProblemsHeaderActions, ProblemsTabs, ProblemsView } from "./ProblemsView";
|
||||
import { UsagesTabs, UsagesView } from "./UsagesView";
|
||||
import { UsagesPopup } from "./UsagesPopup";
|
||||
import { findUsages, showUsageResults, showUsagesPopup } from "./usages";
|
||||
import { findUsages, resetUsages, showUsageResults, showUsagesPopup } from "./usages";
|
||||
import { HierarchyTabs, HierarchyView } from "./HierarchyView";
|
||||
import { showHierarchy } from "./hierarchy";
|
||||
import { resetHierarchy, showHierarchy } from "./hierarchy";
|
||||
import { installServerEvents } from "./servers";
|
||||
import { ServerStatus } from "./ServerStatus";
|
||||
|
||||
@@ -35,6 +35,8 @@ export function activate(): () => void {
|
||||
if (r !== root) {
|
||||
root = r;
|
||||
clearProblems();
|
||||
resetUsages();
|
||||
resetHierarchy();
|
||||
}
|
||||
});
|
||||
const disposers = [
|
||||
|
||||
@@ -126,6 +126,12 @@ export async function fetchUsages(c: CaretContext): Promise<FetchedUsages> {
|
||||
return { groups: groupUsages(locs, files), note };
|
||||
}
|
||||
|
||||
/** The window switched projects: results (and in-flight searches) of the previous one are dropped. */
|
||||
export function resetUsages() {
|
||||
useUsages.setState({ search: null });
|
||||
useUsagesPopup.setState({ open: false, symbol: null, groups: [], note: null });
|
||||
}
|
||||
|
||||
export async function findUsages() {
|
||||
const c = caretContext();
|
||||
if (!c) {
|
||||
|
||||
@@ -67,6 +67,9 @@ export function activate(): () => void {
|
||||
treeReveal: null,
|
||||
manualCommit: new Set(),
|
||||
pendingTx: new Set(),
|
||||
running: {},
|
||||
dialog: null,
|
||||
newMenuOpen: false,
|
||||
});
|
||||
}),
|
||||
registerDatabaseTreeActions(),
|
||||
|
||||
@@ -16,7 +16,7 @@ import { loadSpecialBreakpoints } from "./breakpoints/special";
|
||||
import { breakpointGutter } from "./editor/breakpointGutter";
|
||||
import { executionPoint } from "./editor/execution";
|
||||
import { onHotCodeReplace } from "./hotswap";
|
||||
import { handleDebugEvent, loadWatches, publishContext } from "./state";
|
||||
import { handleDebugEvent, loadWatches, publishContext, resetSessions } from "./state";
|
||||
import { DebuggerOverlays } from "./ui/Overlays";
|
||||
|
||||
export function activate(): () => void {
|
||||
@@ -50,6 +50,7 @@ export function activate(): () => void {
|
||||
useProject.subscribe((s, prev) => {
|
||||
const root = s.project?.root ?? null;
|
||||
if (root !== (prev.project?.root ?? null)) {
|
||||
resetSessions();
|
||||
loadBreakpoints(root);
|
||||
loadExceptionBreakpoints(root);
|
||||
loadSpecialBreakpoints(root);
|
||||
|
||||
@@ -635,6 +635,15 @@ export function handleDebugEvent(id: DebugSessionId, event: DebugEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The window switched projects: stops its sessions and forgets them (late events stay ignored via `owned`). */
|
||||
export function resetSessions() {
|
||||
const { sessions } = useDebug.getState();
|
||||
for (const s of Object.values(sessions)) if (s.parent == null && isLive(s)) void stopSession(s.id);
|
||||
useDebug.setState({ sessions: {}, active: null, console: {}, stopSeq: 0 });
|
||||
useExecutionPoint.setState({ path: null, line: 0, top: true, values: {} });
|
||||
publishContext();
|
||||
}
|
||||
|
||||
/** Breakpoint ↔ session glue: unverified look only makes sense while debugging. */
|
||||
export function sessionsRunning(): boolean {
|
||||
return Object.values(useDebug.getState().sessions).some(isLive);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { registerOverlay, registerToolWindow } from "@/workbench/contributions";
|
||||
import { GithubDialogHost } from "./dialogs";
|
||||
import { checkoutAction, createPullRequest, openInBrowser, showPullRequests, WINDOW } from "./ops";
|
||||
import { PullRequestsHeader, PullRequestsView } from "./PullRequestsView";
|
||||
import { DEFAULT_QUERY, refreshAll, useGithub } from "./store";
|
||||
import { refreshAll, resetGithub } from "./store";
|
||||
|
||||
const WHEN = "projectOpen";
|
||||
|
||||
@@ -65,10 +65,8 @@ export function activate(): () => void {
|
||||
},
|
||||
{ id: "github.refresh", title: "Refresh Pull Requests", category: "GitHub", icon: "refresh", when: WHEN, run: () => refreshAll() },
|
||||
]),
|
||||
// Another project: start over.
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root === prev.project?.root) return;
|
||||
useGithub.setState({ availability: null, list: [], selected: null, details: null, reviewComments: [], query: DEFAULT_QUERY });
|
||||
if (s.project?.root !== prev.project?.root) resetGithub();
|
||||
}),
|
||||
];
|
||||
return () => disposers.reverse().forEach((d) => d());
|
||||
|
||||
@@ -163,6 +163,12 @@ export function cancelBusy() {
|
||||
|
||||
const diffCache = new Map<string, PrDiffRevisions>();
|
||||
|
||||
/** Another project: start over. */
|
||||
export function resetGithub() {
|
||||
diffCache.clear();
|
||||
useGithub.setState({ availability: null, list: [], selected: null, details: null, reviewComments: [], query: DEFAULT_QUERY });
|
||||
}
|
||||
|
||||
/** Fetches what the local repository lacks to diff `pr`, once per head commit. */
|
||||
export async function diffRevisions(pr: PullRequestDetails): Promise<PrDiffRevisions | null> {
|
||||
const key = `${pr.summary.number}:${pr.headOid}:${pr.baseOid}`;
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
import { activeEditor, registerEditorContribution, registerLanguageMode } from "@/editor/api";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { call, commands, emitLocalIdeEvent, IpcError, isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { editorService, notifications, registerServiceView, showServiceView } from "@/workbench/contributions";
|
||||
import { HTTP_LANGUAGE, httpEditorExtension, refreshRequests, requestAtCaret } from "./editor";
|
||||
import { HttpClientHeaderActions, HttpClientView } from "./HttpClientView";
|
||||
import { httpLanguageDescription } from "./language";
|
||||
import { SERVICE_VIEW, runAll, runRequest } from "./store";
|
||||
import { SERVICE_VIEW, resetHttpClient, runAll, runRequest } from "./store";
|
||||
|
||||
export const NEW_REQUEST_TEMPLATE = `### Example request
|
||||
# Variables come from http-client.env.json / http-client.private.env.json
|
||||
@@ -73,6 +74,9 @@ async function newRequest() {
|
||||
|
||||
export function activate(): () => void {
|
||||
const disposers = [
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) resetHttpClient();
|
||||
}),
|
||||
registerLanguageMode(httpLanguageDescription),
|
||||
registerEditorContribution({ id: "http.requests", order: 56, extensions: httpEditorExtension }),
|
||||
registerServiceView({
|
||||
|
||||
@@ -166,3 +166,9 @@ export function cancelRunning() {
|
||||
const r = useHttpClient.getState().running;
|
||||
if (r) void commands.cancelRequest(r.request);
|
||||
}
|
||||
|
||||
/** The window switched projects: history, responses and environments belong to the previous one. */
|
||||
export function resetHttpClient() {
|
||||
cancelRunning();
|
||||
useHttpClient.setState({ environments: {}, history: [], exchanges: {}, selected: null, running: null });
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import { registerExternalTools } from "./externalTools";
|
||||
import { EditConfigsOverlay } from "./EditConfigsDialog";
|
||||
import { RunWidget } from "./RunWidget";
|
||||
import { runGutter } from "./runGutter";
|
||||
import { activateStackTrace } from "./stackTrace";
|
||||
import { activateStackTrace, useStackTraces } from "./stackTrace";
|
||||
import { clearRuntimeCache } from "./runtimes";
|
||||
import {
|
||||
adoptTemporary,
|
||||
@@ -373,6 +373,7 @@ export function activate(): () => void {
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root === prev.project?.root) return;
|
||||
resetRuns();
|
||||
useStackTraces.setState({ tabs: [], dialog: null });
|
||||
if (s.project) void loadConfigs();
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -36,6 +36,11 @@ export const useTracking = create<TrackingState>(() => ({
|
||||
|
||||
const docPaths = new Map<number, string>();
|
||||
|
||||
/** The window switched projects: the previous one's places and edits don't apply. */
|
||||
export function resetTracking() {
|
||||
useTracking.setState({ caret: null, editVersions: new Map(), edited: new Set(), locations: [] });
|
||||
}
|
||||
|
||||
function bumpVersion(path: string, userEdit: boolean) {
|
||||
useTracking.setState((s) => ({
|
||||
editVersions: new Map(s.editVersions).set(path, (s.editVersions.get(path) ?? 0) + 1),
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
} from "./bookmarks/store";
|
||||
import { maybeToolWindowService } from "./common/services";
|
||||
import { openLocation } from "./common/open";
|
||||
import { installTracking } from "./common/tracking";
|
||||
import { installTracking, resetTracking } from "./common/tracking";
|
||||
import { SearchEverywhereOverlay } from "./everywhere/SearchEverywhere";
|
||||
import { hideSearchEverywhere, showSearchEverywhere } from "./everywhere/store";
|
||||
import { FindInFilesOverlay } from "./find/FindInFiles";
|
||||
import { hideFindPopup, parseFindArg, showFindPopup } from "./find/store";
|
||||
import { hideFindPopup, parseFindArg, showFindPopup, useFindPopup } from "./find/store";
|
||||
import { disposeFindWindow } from "./find/windowStore";
|
||||
import { RecentPopupsOverlay } from "./recent/RecentPopups";
|
||||
import { cycleSwitcher, hideRecentPopup, showRecentPopup } from "./recent/store";
|
||||
@@ -294,7 +294,11 @@ export function activate(): void | (() => void) {
|
||||
onProject(useProject.getState().project?.root ?? null);
|
||||
disposers.push(
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) onProject(s.project?.root ?? null);
|
||||
if (s.project?.root === prev.project?.root) return;
|
||||
onProject(s.project?.root ?? null);
|
||||
resetTracking();
|
||||
// A directory scope pointed into the previous project.
|
||||
if (useFindPopup.getState().scopeTab === "directory") useFindPopup.setState({ scopeTab: "project", directory: "" });
|
||||
}),
|
||||
onIdeEvent("fsChanged", (e) => {
|
||||
if (!e.removed.length) return;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { installPreparedMessage, useCommit } from "../commitStore";
|
||||
import { installGitLog, useGitLog } from "../logStore";
|
||||
import { logConsole, useVcsConsole } from "../ops";
|
||||
import { useVcs } from "../store";
|
||||
|
||||
describe("switching the window to another repository", () => {
|
||||
const disposers: Array<() => void> = [];
|
||||
afterEach(() => disposers.splice(0).forEach((d) => d()));
|
||||
|
||||
it("drops the commit message, Amend and the log of the previous one", () => {
|
||||
useVcs.setState({ root: "/a" });
|
||||
disposers.push(installPreparedMessage(), installGitLog());
|
||||
useCommit.setState({ message: "wip", amend: true, selection: ["/a/x"] });
|
||||
useGitLog.setState({ commits: [{ id: "c1" } as never], selected: "c1", selection: ["c1"] });
|
||||
useGitLog.setState((s) => ({ filters: { ...s.filters, author: "me" } }));
|
||||
logConsole("cmd", "git status");
|
||||
|
||||
useVcs.setState({ root: "/b" });
|
||||
|
||||
expect(useCommit.getState()).toMatchObject({ message: "", amend: false, selection: [] });
|
||||
expect(useGitLog.getState()).toMatchObject({ commits: [], selected: null, selection: [] });
|
||||
expect(useGitLog.getState().filters.author).toBe("");
|
||||
expect(useVcsConsole.getState().lines).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,20 @@ export function installPreparedMessage(): () => void {
|
||||
const op = (s = useVcs.getState()) => s.branch?.operation ?? null;
|
||||
if (PREPARED_MESSAGE_OPS.has(op() ?? "")) void load();
|
||||
return useVcs.subscribe((s, prev) => {
|
||||
if (s.root !== prev.root) {
|
||||
// Another repository: nothing typed or ticked for the previous one carries over (Amend least of all).
|
||||
useCommit.setState({
|
||||
message: "",
|
||||
amend: false,
|
||||
checks: new Map(),
|
||||
selection: [],
|
||||
tab: "changes",
|
||||
selectedList: null,
|
||||
selectedShelf: null,
|
||||
});
|
||||
if (PREPARED_MESSAGE_OPS.has(op(s) ?? "")) void load();
|
||||
return;
|
||||
}
|
||||
const now = op(s);
|
||||
if (now !== op(prev) && PREPARED_MESSAGE_OPS.has(now ?? "")) void load();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Branch, BranchInfo, Commit } from "@/bindings";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { computeGraph, emptyGraphState, type GraphRow, type GraphState } from "./graph";
|
||||
import { errorMessage } from "./ops";
|
||||
import { clearConsole, errorMessage } from "./ops";
|
||||
import { useVcs } from "./store";
|
||||
|
||||
export interface LogFilters {
|
||||
@@ -71,12 +71,13 @@ export const useGitLog = create<LogState>(() => ({ ...initialLog(), generation:
|
||||
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
/** Another repository (the window switched projects): forget its commits, filters and selection. */
|
||||
/** Another repository (the window switched projects): forget its commits, filters, selection and console. */
|
||||
export function installGitLog(): () => void {
|
||||
return useVcs.subscribe((s, prev) => {
|
||||
if (s.root === prev.root) return;
|
||||
loadPromise = null;
|
||||
useGitLog.setState((l) => ({ ...initialLog(), tab: l.tab, generation: l.generation + 1 }));
|
||||
clearConsole();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Заблокувати користувача