mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-06 05:38:13 +01:00
sessions: retain Changes details state per session (#330619)
* sessions: retain changes details state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * sessions: rebuild stale changes tree Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
81d13c0686
commit
0a8daffbbb
@@ -136,6 +136,9 @@ Then read the relevant spec for the area you are changing (see table below). If
|
||||
- **`ChatSource` is fully discriminated**: Fork and side-chat sources both require explicit `kind` plus stable top-level `turnId`. Do not add no-kind compatibility helpers or route by structural property presence; switch directly on `source.kind`.
|
||||
- **Sessions menu ids must live in the shared menu registry**: Do not declare sessions-owned `new MenuId(...)` constants ad hoc inside individual parts. Add them to `browser/menus.ts` under `Menus` with discoverable `SessionsEditor...` names so ownership and reuse stay obvious.
|
||||
- **Events instead of observables**: Session state must flow through `IObservable`, not `Event`. Use `autorun`/`derived` for reactive UI, not `onDid*` event listeners.
|
||||
- **Keep feature-specific ObjectTree restoration local**: data trees such as Explorer accept complete view state through `setInput`, but `ObjectTree.setChildren` does not. Do not widen the shared tree widget API for one Sessions consumer; apply saved collapse state to the replacement children, then restore focus, selection, and scroll from identities in the feature.
|
||||
- **Bound retained Changes-details state like editor view state**: keep per-session details snapshots in a 100-entry LRU, move them on draft commit, and clear them only on definitive delete/discard events. Provider `removed` deltas are temporary eviction signals and must not discard durable UI state.
|
||||
- **Do not persist a view snapshot through an independently registered shutdown listener**: service and view-container `onWillSaveState` listener order can save the service before the view captures its final state. Since the Changes tree is sampled only at lifecycle boundaries, persist synchronously when that sample updates the service, and likewise persist replacement/deletion mutations immediately.
|
||||
- **Importing from providers**: Non-provider `contrib/*` code must never import from `contrib/providers/*`. Extract shared interfaces to `services/` or `common/`.
|
||||
- **`IAgentSessionsService` in shared code**: `IAgentSessionsService` (`vs/workbench/contrib/chat/browser/agentSessions/agentSessionsService`) is a Copilot-provider internal and may be imported **only** by the Copilot chat sessions provider (`contrib/providers/copilotChatSessions/`). Shared sessions code (core/services/non-provider contribs, e.g. the sessions list or visible-sessions grid) must stay provider-agnostic and go through `ISession`/`ISessionsManagementService` — never reach into `model.observeSession(...)` etc. for lazy loading. This is enforced by an ESLint `no-restricted-imports` ban scoped to `src/vs/sessions/**` (Copilot provider exempted).
|
||||
- **Missing entry point import**: New contribution files must be imported in the appropriate `sessions.*.main.ts` entry point to be loaded (for example `sessions.common.main.ts`, `sessions.desktop.main.ts`, `sessions.web.main.ts`, or `sessions.web.main.internal.ts`).
|
||||
|
||||
@@ -87,7 +87,7 @@ import { compareFileNames, comparePaths } from '../../../../base/common/comparer
|
||||
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
|
||||
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
|
||||
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { ChangesViewSection, IChangesViewService } from '../common/changesViewService.js';
|
||||
import { ChangesViewSection, IChangesDetailsViewState, IChangesDetailsViewStateTransfer, IChangesViewService } from '../common/changesViewService.js';
|
||||
import { ChangesSummaryWidget } from './changesSummaryWidget.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js';
|
||||
@@ -535,6 +535,8 @@ export class ChangesViewPane extends ViewPane {
|
||||
|
||||
private changesProgressBar!: ProgressBar;
|
||||
private tree: WorkbenchCompressibleObjectTree<ChangesTreeElement> | undefined;
|
||||
private renderedTreeState: { readonly sessionResource: URI; readonly viewMode: ChangesViewMode } | undefined;
|
||||
private detailsViewStateTransfer: IChangesDetailsViewStateTransfer | undefined;
|
||||
private ciStatusWidget: CIStatusWidget | undefined;
|
||||
private sessionFilesWidget: SessionFilesWidget | undefined;
|
||||
private splitView: SplitView | undefined;
|
||||
@@ -811,6 +813,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
if (visible) {
|
||||
this.onVisible();
|
||||
} else {
|
||||
this.captureDetailsViewState();
|
||||
this.renderDisposables.clear();
|
||||
}
|
||||
}));
|
||||
@@ -979,15 +982,38 @@ export class ChangesViewPane extends ViewPane {
|
||||
this.renderDisposables.add(autorun(reader => {
|
||||
const changes = changesObs.read(reader);
|
||||
const viewMode = this.changesViewService.viewModeObs.read(reader);
|
||||
const changesetLoading = this.changesViewService.activeSessionChangesetLoadingObs.read(reader);
|
||||
const activeSessionLoading = this.changesViewService.activeSessionLoadingObs.read(reader);
|
||||
const sessionResource = this.changesViewService.activeSessionResourceObs.read(reader);
|
||||
|
||||
// Read session state so this autorun re-runs when git state (e.g. branch
|
||||
// name) arrives asynchronously, since the tree root label depends on it.
|
||||
this.changesViewService.activeSessionStateObs.read(reader);
|
||||
|
||||
if (!this.tree || changesetLoading) {
|
||||
if (!this.tree || activeSessionLoading) {
|
||||
return;
|
||||
}
|
||||
const detailsViewStateTransfer = this.changesViewService.detailsViewStateTransferObs.read(reader);
|
||||
if (detailsViewStateTransfer !== this.detailsViewStateTransfer) {
|
||||
this.detailsViewStateTransfer = detailsViewStateTransfer;
|
||||
if (detailsViewStateTransfer && this.renderedTreeState) {
|
||||
const renderedSessionResource = this.renderedTreeState.sessionResource;
|
||||
if (isEqual(renderedSessionResource, detailsViewStateTransfer.from)) {
|
||||
this.captureDetailsViewState(detailsViewStateTransfer.to);
|
||||
this.renderedTreeState = undefined;
|
||||
if (sessionResource && isEqual(sessionResource, detailsViewStateTransfer.from)) {
|
||||
return;
|
||||
}
|
||||
} else if (!isEqual(renderedSessionResource, detailsViewStateTransfer.to)) {
|
||||
this.captureDetailsViewState();
|
||||
if (sessionResource && isEqual(sessionResource, renderedSessionResource)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.captureDetailsViewState();
|
||||
}
|
||||
const detailsViewState = sessionResource ? this.changesViewService.getDetailsViewState(sessionResource, viewMode) : undefined;
|
||||
|
||||
// Toggle list-mode class to remove tree indentation in list mode
|
||||
this.listContainer?.classList.toggle('list-mode', viewMode === ChangesViewMode.List);
|
||||
@@ -996,14 +1022,14 @@ export class ChangesViewPane extends ViewPane {
|
||||
// Tree mode: build hierarchical tree from file entries
|
||||
const treeRootInfo = this.getTreeRootInfo(changes);
|
||||
const treeChildren = buildTreeChildren(changes, treeRootInfo);
|
||||
this.tree.setChildren(null, treeChildren);
|
||||
this.setDetailsTreeChildren(sessionResource, viewMode, detailsViewState, treeChildren);
|
||||
} else {
|
||||
// List mode: flat list of file items
|
||||
const listChildren = changes.map(item => ({
|
||||
element: item,
|
||||
collapsible: false,
|
||||
} satisfies IObjectTreeElement<ChangesTreeElement>));
|
||||
this.tree.setChildren(null, listChildren);
|
||||
this.setDetailsTreeChildren(sessionResource, viewMode, detailsViewState, listChildren);
|
||||
}
|
||||
|
||||
this.fireTreePaneSizeChange();
|
||||
@@ -1011,6 +1037,60 @@ export class ChangesViewPane extends ViewPane {
|
||||
}));
|
||||
}
|
||||
|
||||
override saveState(): void {
|
||||
this.captureDetailsViewState();
|
||||
super.saveState();
|
||||
}
|
||||
|
||||
private captureDetailsViewState(sessionResource?: URI): void {
|
||||
if (!this.tree || !this.renderedTreeState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.tree.getViewState().toJSON();
|
||||
this.changesViewService.setDetailsViewState(sessionResource ?? this.renderedTreeState.sessionResource, this.renderedTreeState.viewMode, {
|
||||
...state,
|
||||
focus: Array.from(state.focus),
|
||||
selection: Array.from(state.selection),
|
||||
});
|
||||
}
|
||||
|
||||
private setDetailsTreeChildren(sessionResource: URI | undefined, viewMode: ChangesViewMode, state: IChangesDetailsViewState | undefined, children: readonly IObjectTreeElement<ChangesTreeElement>[]): void {
|
||||
if (!this.tree) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementsById = new Map<string, ChangesTreeElement>();
|
||||
const restoredChildren = this.applyDetailsViewState(children, state, elementsById);
|
||||
|
||||
this.renderedTreeState = undefined;
|
||||
this.tree.setChildren(null, restoredChildren);
|
||||
this.tree.setFocus(state ? Array.from(state.focus, id => elementsById.get(id)).filter(element => element !== undefined) : []);
|
||||
this.tree.setSelection(state ? Array.from(state.selection, id => elementsById.get(id)).filter(element => element !== undefined) : []);
|
||||
this.tree.scrollTop = state?.scrollTop ?? 0;
|
||||
this.renderedTreeState = sessionResource ? { sessionResource, viewMode } : undefined;
|
||||
}
|
||||
|
||||
private applyDetailsViewState(
|
||||
children: readonly IObjectTreeElement<ChangesTreeElement>[],
|
||||
state: IChangesDetailsViewState | undefined,
|
||||
elementsById: Map<string, ChangesTreeElement>,
|
||||
): IObjectTreeElement<ChangesTreeElement>[] {
|
||||
return children.map(child => {
|
||||
const id = child.element.uri.toString();
|
||||
elementsById.set(id, child.element);
|
||||
const restoredChildren = child.children
|
||||
? this.applyDetailsViewState(Array.from(child.children), state, elementsById)
|
||||
: undefined;
|
||||
const expanded = state?.expanded[id];
|
||||
return {
|
||||
...child,
|
||||
children: restoredChildren,
|
||||
collapsed: expanded === undefined ? child.collapsed : expanded === 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _bindContextKeys(topLevelStats: IObservable<{ files: number } | undefined>): void {
|
||||
// Request in progress (can be updated independently since it only affects action enablement, and not visibility)
|
||||
this.renderDisposables.add(bindContextKey(ChatContextKeys.requestInProgress, this.scopedContextKeyService, reader => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { structuralEquals } from '../../../../base/common/equals.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ResourceMap } from '../../../../base/common/map.js';
|
||||
import { LRUCache, ResourceMap } from '../../../../base/common/map.js';
|
||||
import { autorun, derived, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, observableSignal, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
@@ -20,7 +20,7 @@ import { ISessionsManagementService } from '../../../services/sessions/common/se
|
||||
import { AgentFeedbackState, IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js';
|
||||
import { ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js';
|
||||
import { ChangesViewMode, IsolationMode } from '../common/changes.js';
|
||||
import { ActiveSessionState, ChangesViewSection, IChangesViewSectionCollapseState, IChangesViewService } from '../common/changesViewService.js';
|
||||
import { ActiveSessionState, ChangesViewSection, IChangesDetailsViewState, IChangesDetailsViewStateTransfer, IChangesViewSectionCollapseState, IChangesViewService } from '../common/changesViewService.js';
|
||||
|
||||
export const ChangesetReviewSupportContext = new RawContextKey<boolean>('sessions.changesetReviewSupport', false);
|
||||
export const ChangesetReviewedFilesContext = new RawContextKey<string[]>('sessions.changesetReviewedFiles', []);
|
||||
@@ -31,6 +31,14 @@ const DEFAULT_SECTION_COLLAPSE_STATE: IChangesViewSectionCollapseState = Object.
|
||||
checks: true,
|
||||
});
|
||||
|
||||
interface IStoredChangesViewState {
|
||||
readonly sessionResource: string;
|
||||
readonly detailsViewState?: Partial<Record<ChangesViewMode, IChangesDetailsViewState>>;
|
||||
}
|
||||
|
||||
const SESSION_VIEW_STATE_STORAGE_KEY = 'changesView.sessionViewState';
|
||||
const SESSION_VIEW_STATE_LIMIT = 100;
|
||||
|
||||
export class ChangesViewService extends Disposable implements IChangesViewService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
@@ -53,6 +61,8 @@ export class ChangesViewService extends Disposable implements IChangesViewServic
|
||||
|
||||
private readonly _sectionCollapseStateBySession = new ResourceMap<IChangesViewSectionCollapseState>();
|
||||
private readonly _sectionCollapseStateChanged = observableSignal('changesView.sectionCollapseStateChanged');
|
||||
private readonly _detailsViewStateBySession = new LRUCache<string, Partial<Record<ChangesViewMode, IChangesDetailsViewState>>>(SESSION_VIEW_STATE_LIMIT);
|
||||
readonly detailsViewStateTransferObs = observableValue<IChangesDetailsViewStateTransfer | undefined>(this, undefined);
|
||||
|
||||
private readonly _selectedChangesetId = observableValue<string | undefined>(this, undefined);
|
||||
setChangesetId(changesetId: string | undefined): void {
|
||||
@@ -78,6 +88,7 @@ export class ChangesViewService extends Disposable implements IChangesViewServic
|
||||
@ISessionsManagementService sessionsManagementService: ISessionsManagementService,
|
||||
) {
|
||||
super();
|
||||
this._loadViewState();
|
||||
|
||||
// Active session resource
|
||||
this.activeSessionResourceObs = derivedOpts({ equalsFn: isEqual }, reader => {
|
||||
@@ -208,19 +219,26 @@ export class ChangesViewService extends Disposable implements IChangesViewServic
|
||||
this.setChangesetId(undefined);
|
||||
}));
|
||||
this._register(sessionsManagementService.onDidReplaceSession(({ from, to }) => {
|
||||
const state = this._sectionCollapseStateBySession.get(from.resource);
|
||||
if (!state) {
|
||||
return;
|
||||
const sectionCollapseState = this._sectionCollapseStateBySession.get(from.resource);
|
||||
if (sectionCollapseState) {
|
||||
this._sectionCollapseStateBySession.delete(from.resource);
|
||||
this._sectionCollapseStateBySession.set(to.resource, sectionCollapseState);
|
||||
this._sectionCollapseStateChanged.trigger(undefined);
|
||||
}
|
||||
this._sectionCollapseStateBySession.delete(from.resource);
|
||||
this._sectionCollapseStateBySession.set(to.resource, state);
|
||||
this._sectionCollapseStateChanged.trigger(undefined);
|
||||
|
||||
const detailsViewState = this._detailsViewStateBySession.get(from.resource.toString());
|
||||
if (detailsViewState) {
|
||||
this._detailsViewStateBySession.delete(from.resource.toString());
|
||||
this._detailsViewStateBySession.set(to.resource.toString(), detailsViewState);
|
||||
this._saveViewState();
|
||||
}
|
||||
this.detailsViewStateTransferObs.set({ from: from.resource, to: to.resource }, undefined);
|
||||
}));
|
||||
this._register(sessionsManagementService.onDidDeleteSession(session => {
|
||||
this._deleteSectionCollapseState(session.resource);
|
||||
this._deleteSessionViewState(session.resource);
|
||||
}));
|
||||
this._register(sessionsManagementService.onDidDiscardNewSession(session => this._deleteSectionCollapseState(session.resource)));
|
||||
this._register(sessionsManagementService.onDidReplaceNewDraftSession(({ from }) => this._deleteSectionCollapseState(from.resource)));
|
||||
this._register(sessionsManagementService.onDidDiscardNewSession(session => this._deleteSessionViewState(session.resource)));
|
||||
this._register(sessionsManagementService.onDidReplaceNewDraftSession(({ from }) => this._deleteSessionViewState(from.resource)));
|
||||
|
||||
// Global context keys
|
||||
this._bindContextKeys();
|
||||
@@ -241,10 +259,62 @@ export class ChangesViewService extends Disposable implements IChangesViewServic
|
||||
this._sectionCollapseStateChanged.trigger(undefined);
|
||||
}
|
||||
|
||||
private _deleteSectionCollapseState(sessionResource: URI): void {
|
||||
getDetailsViewState(sessionResource: URI, viewMode: ChangesViewMode): IChangesDetailsViewState | undefined {
|
||||
return this._detailsViewStateBySession.get(sessionResource.toString())?.[viewMode];
|
||||
}
|
||||
|
||||
setDetailsViewState(sessionResource: URI, viewMode: ChangesViewMode, state: IChangesDetailsViewState): void {
|
||||
const key = sessionResource.toString();
|
||||
const current = this._detailsViewStateBySession.get(key);
|
||||
if (structuralEquals(current?.[viewMode], state)) {
|
||||
return;
|
||||
}
|
||||
this._detailsViewStateBySession.set(key, { ...current, [viewMode]: state });
|
||||
this._saveViewState();
|
||||
}
|
||||
|
||||
private _deleteSessionViewState(sessionResource: URI): void {
|
||||
if (this._sectionCollapseStateBySession.delete(sessionResource)) {
|
||||
this._sectionCollapseStateChanged.trigger(undefined);
|
||||
}
|
||||
if (this._detailsViewStateBySession.delete(sessionResource.toString())) {
|
||||
this._saveViewState();
|
||||
}
|
||||
}
|
||||
|
||||
private _loadViewState(): void {
|
||||
const entries = this.storageService.getObject<IStoredChangesViewState[]>(SESSION_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE, []);
|
||||
if (!Array.isArray(entries)) {
|
||||
this.storageService.remove(SESSION_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (typeof entry.sessionResource !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resource = URI.parse(entry.sessionResource);
|
||||
if (entry.detailsViewState) {
|
||||
this._detailsViewStateBySession.set(resource.toString(), entry.detailsViewState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _saveViewState(): void {
|
||||
if (this._detailsViewStateBySession.size === 0) {
|
||||
this.storageService.remove(SESSION_VIEW_STATE_STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries: IStoredChangesViewState[] = [];
|
||||
this._detailsViewStateBySession.forEach((detailsViewState, sessionResource) => {
|
||||
entries.push({
|
||||
sessionResource,
|
||||
detailsViewState,
|
||||
});
|
||||
});
|
||||
this.storageService.store(SESSION_VIEW_STATE_STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
}
|
||||
|
||||
setChangesetFilesReviewState(resources: readonly URI[], reviewed: boolean): void {
|
||||
|
||||
@@ -35,6 +35,18 @@ export interface IChangesViewSectionCollapseState {
|
||||
|
||||
export type ChangesViewSection = keyof IChangesViewSectionCollapseState;
|
||||
|
||||
export interface IChangesDetailsViewState {
|
||||
readonly focus: readonly string[];
|
||||
readonly selection: readonly string[];
|
||||
readonly expanded: Readonly<Record<string, 0 | 1>>;
|
||||
readonly scrollTop: number;
|
||||
}
|
||||
|
||||
export interface IChangesDetailsViewStateTransfer {
|
||||
readonly from: URI;
|
||||
readonly to: URI;
|
||||
}
|
||||
|
||||
export interface IChangesViewService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -59,6 +71,9 @@ export interface IChangesViewService {
|
||||
readonly viewModeObs: IObservable<ChangesViewMode>;
|
||||
setViewMode(mode: ChangesViewMode): void;
|
||||
setSectionCollapsed(sessionResource: URI, section: ChangesViewSection, collapsed: boolean): void;
|
||||
readonly detailsViewStateTransferObs: IObservable<IChangesDetailsViewStateTransfer | undefined>;
|
||||
getDetailsViewState(sessionResource: URI, viewMode: ChangesViewMode): IChangesDetailsViewState | undefined;
|
||||
setDetailsViewState(sessionResource: URI, viewMode: ChangesViewMode, state: IChangesDetailsViewState): void;
|
||||
|
||||
setChangesetFilesReviewState(resources: readonly URI[], reviewed: boolean): void;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ISessionsService } from '../../../../services/sessions/browser/sessions
|
||||
import { IAgentFeedbackService } from '../../../agentFeedback/browser/agentFeedbackService.js';
|
||||
import { ICodeReviewService, PRReviewStateKind } from '../../../codeReview/browser/codeReviewService.js';
|
||||
import { ChangesViewService } from '../../browser/changesViewService.js';
|
||||
import { ChangesViewMode } from '../../common/changes.js';
|
||||
|
||||
suite('ChangesViewService', () => {
|
||||
|
||||
@@ -59,7 +60,7 @@ suite('ChangesViewService', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function createHarness(initialSession: IActiveSession) {
|
||||
function createHarness(initialSession: IActiveSession, storageService = disposables.add(new TestStorageService())) {
|
||||
const activeSession = observableValue<IActiveSession | undefined>('test.activeSession', initialSession);
|
||||
const onDidReplaceSession = disposables.add(new Emitter<{ readonly from: ISession; readonly to: ISession }>());
|
||||
const onDidDeleteSession = disposables.add(new Emitter<ISession>());
|
||||
@@ -89,11 +90,11 @@ suite('ChangesViewService', () => {
|
||||
codeReviewService,
|
||||
disposables.add(new MockContextKeyService()),
|
||||
sessionsService,
|
||||
disposables.add(new TestStorageService()),
|
||||
storageService,
|
||||
sessionsManagementService,
|
||||
));
|
||||
|
||||
return { activeSession, onDidDeleteSession, onDidDiscardNewSession, onDidReplaceNewDraftSession, onDidReplaceSession, service };
|
||||
return { activeSession, onDidDeleteSession, onDidDiscardNewSession, onDidReplaceNewDraftSession, onDidReplaceSession, service, storageService };
|
||||
}
|
||||
|
||||
test('restores section collapse state independently per session', () => {
|
||||
@@ -125,17 +126,30 @@ suite('ChangesViewService', () => {
|
||||
const draft = createSession('draft');
|
||||
const committed = createSession('committed');
|
||||
const { activeSession, onDidDeleteSession, onDidReplaceSession, service } = createHarness(draft);
|
||||
const detailsViewState = {
|
||||
focus: [],
|
||||
selection: [],
|
||||
expanded: {},
|
||||
scrollTop: 40,
|
||||
};
|
||||
|
||||
service.setSectionCollapsed(draft.resource, 'otherFiles', true);
|
||||
service.setDetailsViewState(draft.resource, ChangesViewMode.List, detailsViewState);
|
||||
activeSession.set(committed, undefined);
|
||||
onDidReplaceSession.fire({ from: draft, to: committed });
|
||||
const afterReplacement = service.activeSessionSectionCollapseStateObs.get();
|
||||
const detailsAfterReplacement = service.getDetailsViewState(committed.resource, ChangesViewMode.List);
|
||||
const detailsViewStateTransfer = service.detailsViewStateTransferObs.get();
|
||||
onDidDeleteSession.fire(committed);
|
||||
const afterDeletion = service.activeSessionSectionCollapseStateObs.get();
|
||||
const detailsAfterDeletion = service.getDetailsViewState(committed.resource, ChangesViewMode.List);
|
||||
|
||||
assert.deepStrictEqual({ afterReplacement, afterDeletion }, {
|
||||
assert.deepStrictEqual({ afterReplacement, detailsAfterReplacement, detailsViewStateTransfer, afterDeletion, detailsAfterDeletion }, {
|
||||
afterReplacement: { otherFiles: true, checks: true },
|
||||
detailsAfterReplacement: detailsViewState,
|
||||
detailsViewStateTransfer: { from: draft.resource, to: committed.resource },
|
||||
afterDeletion: { otherFiles: false, checks: true },
|
||||
detailsAfterDeletion: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,6 +172,86 @@ suite('ChangesViewService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('restores details view state independently per session and view mode', () => {
|
||||
const sessionA = createSession('a');
|
||||
const sessionB = createSession('b');
|
||||
const { service } = createHarness(sessionA);
|
||||
const listState = {
|
||||
focus: ['file:///repo/a.ts'],
|
||||
selection: ['file:///repo/a.ts'],
|
||||
expanded: {},
|
||||
scrollTop: 80,
|
||||
};
|
||||
const treeState = {
|
||||
focus: [],
|
||||
selection: [],
|
||||
expanded: { 'file:///repo/src': 0 as const },
|
||||
scrollTop: 120,
|
||||
};
|
||||
|
||||
service.setDetailsViewState(sessionA.resource, ChangesViewMode.List, listState);
|
||||
service.setDetailsViewState(sessionA.resource, ChangesViewMode.Tree, treeState);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
sessionAList: service.getDetailsViewState(sessionA.resource, ChangesViewMode.List),
|
||||
sessionATree: service.getDetailsViewState(sessionA.resource, ChangesViewMode.Tree),
|
||||
sessionBList: service.getDetailsViewState(sessionB.resource, ChangesViewMode.List),
|
||||
}, {
|
||||
sessionAList: listState,
|
||||
sessionATree: treeState,
|
||||
sessionBList: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('retains details view state for the 100 most recently used sessions', () => {
|
||||
const firstSession = createSession('0');
|
||||
const { service } = createHarness(firstSession);
|
||||
const state = {
|
||||
focus: [],
|
||||
selection: [],
|
||||
expanded: {},
|
||||
scrollTop: 0,
|
||||
};
|
||||
|
||||
for (let i = 0; i <= 100; i++) {
|
||||
service.setDetailsViewState(createSession(`${i}`).resource, ChangesViewMode.List, state);
|
||||
}
|
||||
|
||||
assert.deepStrictEqual({
|
||||
first: service.getDetailsViewState(firstSession.resource, ChangesViewMode.List),
|
||||
last: service.getDetailsViewState(createSession('100').resource, ChangesViewMode.List),
|
||||
}, {
|
||||
first: undefined,
|
||||
last: state,
|
||||
});
|
||||
});
|
||||
|
||||
test('persists Changes view state mutations immediately', () => {
|
||||
const draft = createSession('draft');
|
||||
const committed = createSession('committed');
|
||||
const storageService = disposables.add(new TestStorageService());
|
||||
const firstHarness = createHarness(draft, storageService);
|
||||
const detailsViewState = {
|
||||
focus: ['file:///repo/a.ts'],
|
||||
selection: ['file:///repo/a.ts'],
|
||||
expanded: { 'file:///repo/src': 0 as const },
|
||||
scrollTop: 64,
|
||||
};
|
||||
|
||||
firstHarness.service.setDetailsViewState(draft.resource, ChangesViewMode.Tree, detailsViewState);
|
||||
firstHarness.onDidReplaceSession.fire({ from: draft, to: committed });
|
||||
firstHarness.service.dispose();
|
||||
|
||||
const restoredService = createHarness(committed, storageService).service;
|
||||
assert.deepStrictEqual({
|
||||
detailsViewState: restoredService.getDetailsViewState(committed.resource, ChangesViewMode.Tree),
|
||||
draftDetailsViewState: restoredService.getDetailsViewState(draft.resource, ChangesViewMode.Tree),
|
||||
}, {
|
||||
detailsViewState,
|
||||
draftDetailsViewState: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('hides the Agent Host merge operation when the base branch is protected', () => {
|
||||
const operations: readonly ISessionChangesetOperation[] = [
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ import { FixtureMenuService } from '../chat/chatFixtureUtils.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js';
|
||||
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { ActiveSessionState, ChangesViewSection, IChangesViewSectionCollapseState, IChangesViewService } from '../../../../../sessions/contrib/changes/common/changesViewService.js';
|
||||
import { ActiveSessionState, ChangesViewSection, IChangesDetailsViewState, IChangesViewSectionCollapseState, IChangesViewService } from '../../../../../sessions/contrib/changes/common/changesViewService.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import { CHANGES_VIEW_CONTAINER_ID, CHANGES_VIEW_ID, ChangesViewMode, IsolationMode } from '../../../../../sessions/contrib/changes/common/changes.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
@@ -85,6 +85,7 @@ class FixtureChangesViewService extends Disposable implements IChangesViewServic
|
||||
readonly activeSessionStateObs: IObservable<ActiveSessionState | undefined>;
|
||||
readonly activeSessionLoadingObs: IObservable<boolean>;
|
||||
readonly activeSessionSectionCollapseStateObs: IObservable<IChangesViewSectionCollapseState>;
|
||||
readonly detailsViewStateTransferObs = constObservable(undefined);
|
||||
readonly viewModeObs = observableValue<ChangesViewMode>(this, ChangesViewMode.List);
|
||||
|
||||
constructor(session: IActiveSession, options: IChangesViewFixtureOptions) {
|
||||
@@ -126,6 +127,10 @@ class FixtureChangesViewService extends Disposable implements IChangesViewServic
|
||||
|
||||
setSectionCollapsed(_sessionResource: URI, _section: ChangesViewSection, _collapsed: boolean): void { }
|
||||
|
||||
getDetailsViewState(_sessionResource: URI, _viewMode: ChangesViewMode): IChangesDetailsViewState | undefined { return undefined; }
|
||||
|
||||
setDetailsViewState(_sessionResource: URI, _viewMode: ChangesViewMode, _state: IChangesDetailsViewState): void { }
|
||||
|
||||
setChangesetId(_changesetId: string | undefined): void { }
|
||||
|
||||
setChangesetFilesReviewState(_resources: readonly URI[], _reviewed: boolean): void { }
|
||||
|
||||
Reference in New Issue
Block a user