From cb2b1cdb29184cb6a63231596018923e8f9e02ee Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Wed, 15 Apr 2026 00:44:18 +0000 Subject: [PATCH 1/7] Implement persistence for tree filters --- src/commands/filterTree.ts | 65 +++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/src/commands/filterTree.ts b/src/commands/filterTree.ts index fb5f0aa6..1cb70fc3 100644 --- a/src/commands/filterTree.ts +++ b/src/commands/filterTree.ts @@ -14,6 +14,7 @@ interface TreeFilterState { } const treeFilters = new Map(); +const filterMementoKeyPrefix = "vscode-containers.filters"; // Only support filtering for containers and images const contextKeys: Partial> = { @@ -25,16 +26,48 @@ export function getTreeFilter(treePrefix: TreePrefix): TreeFilterState { return treeFilters.get(treePrefix) || { filterText: "", isActive: false }; } -function setTreeFilter(treePrefix: TreePrefix, filterText: string): void { +function getFilterMementoKey(treePrefix: TreePrefix): string { + return `${filterMementoKeyPrefix}.${treePrefix}`; +} + +function loadPersistedTreeFilter(treePrefix: TreePrefix): void { + const persistedFilter = ext.context.workspaceState.get( + getFilterMementoKey(treePrefix) + ); + + if (persistedFilter) { + treeFilters.set(treePrefix, { + filterText: persistedFilter.toLowerCase(), + isActive: true, + }); + } else { + treeFilters.delete(treePrefix); + } +} + +async function setTreeFilter( + treePrefix: TreePrefix, + filterText: string +): Promise { + const normalizedFilterText = filterText.toLowerCase(); + treeFilters.set(treePrefix, { - filterText: filterText.toLowerCase(), - isActive: filterText.length > 0, + filterText: normalizedFilterText, + isActive: normalizedFilterText.length > 0, }); - setFilterContextValue(treePrefix, filterText.length > 0); + await ext.context.workspaceState.update( + getFilterMementoKey(treePrefix), + normalizedFilterText || undefined + ); + setFilterContextValue(treePrefix, normalizedFilterText.length > 0); } -function clearTreeFilter(treePrefix: TreePrefix): void { - treeFilters.set(treePrefix, { filterText: "", isActive: false }); +async function clearTreeFilter(treePrefix: TreePrefix): Promise { + treeFilters.delete(treePrefix); + await ext.context.workspaceState.update( + getFilterMementoKey(treePrefix), + undefined + ); setFilterContextValue(treePrefix, false); } @@ -47,8 +80,10 @@ function setFilterContextValue(treePrefix: TreePrefix, value: boolean): void { export function setInitialFilterContextValues(): void { for (const treePrefix of Object.keys(contextKeys) as TreePrefix[]) { + loadPersistedTreeFilter(treePrefix); const filter = getTreeFilter(treePrefix); setFilterContextValue(treePrefix, filter.isActive); + updateTreeViewTitle(treePrefix); } } @@ -121,25 +156,25 @@ async function filterTreeView( ]; } - quickPick.onDidAccept(() => { + quickPick.onDidAccept(async () => { const value = quickPick.value.trim(); const selectedItem = quickPick.selectedItems[0]; // Check if "Clear Filter" was selected if (selectedItem?.label === clearFilterLabel) { - clearTreeFilter(treePrefix); + await clearTreeFilter(treePrefix); context.telemetry.properties.action = "clearFilter"; } else if (value) { - setTreeFilter(treePrefix, value); + await setTreeFilter(treePrefix, value); context.telemetry.properties.action = "applyFilter"; context.telemetry.properties.filterLength = value.length.toString(); } else { - clearTreeFilter(treePrefix); + await clearTreeFilter(treePrefix); context.telemetry.properties.action = "clearFilter"; } quickPick.hide(); - void refreshTreeView(treePrefix); + await refreshTreeView(treePrefix); }); quickPick.onDidHide(() => { @@ -223,15 +258,15 @@ export async function filterImagesTree(context: IActionContext): Promise { export async function clearContainersFilter( context: IActionContext ): Promise { - clearTreeFilter("containers"); + await clearTreeFilter("containers"); context.telemetry.properties.action = "clearFilter"; - void refreshTreeView("containers"); + await refreshTreeView("containers"); } export async function clearImagesFilter( context: IActionContext ): Promise { - clearTreeFilter("images"); + await clearTreeFilter("images"); context.telemetry.properties.action = "clearFilter"; - void refreshTreeView("images"); + await refreshTreeView("images"); } From c703eb6693ddf4e335c6221588c5a2b9d16d7976 Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Wed, 15 Apr 2026 00:51:54 +0000 Subject: [PATCH 2/7] Refactor TestMemento storage type and improve update logic --- src/test/TestMemento.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/test/TestMemento.ts b/src/test/TestMemento.ts index f508bbf1..0094eb8c 100644 --- a/src/test/TestMemento.ts +++ b/src/test/TestMemento.ts @@ -6,18 +6,23 @@ import * as vscode from 'vscode'; export class TestMemento implements vscode.Memento { - private readonly values: { [key: string]: never } = {}; + private readonly values: Record = {}; keys(): readonly string[] { return Object.keys(this.values); } get(key: string, defaultValue?: T): T | undefined { - return this.values[key] ?? defaultValue; + return (this.values[key] as T | undefined) ?? defaultValue; } - update(key: string, value: never): Thenable { - this.values[key] = value; + update(key: string, value: unknown): Thenable { + if (value === undefined) { + delete this.values[key]; + } else { + this.values[key] = value; + } + return Promise.resolve(); } } From 6729c9a00b6a0cd812dbd08a8d9f6a3876749c5c Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Wed, 15 Apr 2026 01:07:03 +0000 Subject: [PATCH 3/7] fix(tree): init display settings before filtering --- src/tree/LocalRootTreeItemBase.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/tree/LocalRootTreeItemBase.ts b/src/tree/LocalRootTreeItemBase.ts index 1c93155d..87eb4b7b 100644 --- a/src/tree/LocalRootTreeItemBase.ts +++ b/src/tree/LocalRootTreeItemBase.ts @@ -97,6 +97,14 @@ export abstract class LocalRootTreeItemBase Date: Wed, 15 Apr 2026 10:07:02 -0400 Subject: [PATCH 4/7] Handle empty persisted tree filters --- src/commands/filterTree.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commands/filterTree.ts b/src/commands/filterTree.ts index 1cb70fc3..eb21b79d 100644 --- a/src/commands/filterTree.ts +++ b/src/commands/filterTree.ts @@ -35,10 +35,11 @@ function loadPersistedTreeFilter(treePrefix: TreePrefix): void { getFilterMementoKey(treePrefix) ); - if (persistedFilter) { + if (persistedFilter !== undefined) { + const normalizedFilter = persistedFilter.toLowerCase(); treeFilters.set(treePrefix, { - filterText: persistedFilter.toLowerCase(), - isActive: true, + filterText: normalizedFilter, + isActive: normalizedFilter.length > 0, }); } else { treeFilters.delete(treePrefix); From a36049074f0004a4fdb231cd02c41bcba2eaccd3 Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Wed, 15 Apr 2026 10:09:59 -0400 Subject: [PATCH 5/7] Catch filter QuickPick accept errors --- src/commands/filterTree.ts | 45 ++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/commands/filterTree.ts b/src/commands/filterTree.ts index eb21b79d..19d5d862 100644 --- a/src/commands/filterTree.ts +++ b/src/commands/filterTree.ts @@ -157,25 +157,32 @@ async function filterTreeView( ]; } - quickPick.onDidAccept(async () => { - const value = quickPick.value.trim(); - const selectedItem = quickPick.selectedItems[0]; - - // Check if "Clear Filter" was selected - if (selectedItem?.label === clearFilterLabel) { - await clearTreeFilter(treePrefix); - context.telemetry.properties.action = "clearFilter"; - } else if (value) { - await setTreeFilter(treePrefix, value); - context.telemetry.properties.action = "applyFilter"; - context.telemetry.properties.filterLength = value.length.toString(); - } else { - await clearTreeFilter(treePrefix); - context.telemetry.properties.action = "clearFilter"; - } - - quickPick.hide(); - await refreshTreeView(treePrefix); + quickPick.onDidAccept(() => { + void (async () => { + const value = quickPick.value.trim(); + const selectedItem = quickPick.selectedItems[0]; + + // Check if "Clear Filter" was selected + if (selectedItem?.label === clearFilterLabel) { + await clearTreeFilter(treePrefix); + context.telemetry.properties.action = "clearFilter"; + } else if (value) { + await setTreeFilter(treePrefix, value); + context.telemetry.properties.action = "applyFilter"; + context.telemetry.properties.filterLength = + value.length.toString(); + } else { + await clearTreeFilter(treePrefix); + context.telemetry.properties.action = "clearFilter"; + } + + quickPick.hide(); + await refreshTreeView(treePrefix); + })().catch((error) => { + void vscode.window.showErrorMessage( + vscode.l10n.t("Failed to apply filter: {0}", String(error)) + ); + }); }); quickPick.onDidHide(() => { From aaab94c1585d2e605fe6966b7c74bebd303c06ad Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Thu, 23 Jul 2026 15:58:32 -0400 Subject: [PATCH 6/7] Address PR feedback in loadPersistedTreeFilter --- .../vscode-containers/src/commands/filterTree.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/extensions/vscode-containers/src/commands/filterTree.ts b/extensions/vscode-containers/src/commands/filterTree.ts index 19d5d862..0450055c 100644 --- a/extensions/vscode-containers/src/commands/filterTree.ts +++ b/extensions/vscode-containers/src/commands/filterTree.ts @@ -31,18 +31,17 @@ function getFilterMementoKey(treePrefix: TreePrefix): string { } function loadPersistedTreeFilter(treePrefix: TreePrefix): void { - const persistedFilter = ext.context.workspaceState.get( - getFilterMementoKey(treePrefix) - ); + const persistedFilter = ext.context.workspaceState + .get(getFilterMementoKey(treePrefix)) + ?.toLowerCase(); - if (persistedFilter !== undefined) { - const normalizedFilter = persistedFilter.toLowerCase(); + if (persistedFilter) { treeFilters.set(treePrefix, { - filterText: normalizedFilter, - isActive: normalizedFilter.length > 0, + filterText: persistedFilter, + isActive: true, }); } else { - treeFilters.delete(treePrefix); + void clearTreeFilter(treePrefix); } } From bc3f3f32376412df16b18f0b4a293aa12772cea8 Mon Sep 17 00:00:00 2001 From: Phill Johntony Date: Thu, 23 Jul 2026 16:31:30 -0400 Subject: [PATCH 7/7] Simplify filter loading and make set/clear sync --- .../src/commands/filterTree.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/extensions/vscode-containers/src/commands/filterTree.ts b/extensions/vscode-containers/src/commands/filterTree.ts index 0450055c..90a1af3f 100644 --- a/extensions/vscode-containers/src/commands/filterTree.ts +++ b/extensions/vscode-containers/src/commands/filterTree.ts @@ -41,30 +41,27 @@ function loadPersistedTreeFilter(treePrefix: TreePrefix): void { isActive: true, }); } else { - void clearTreeFilter(treePrefix); + clearTreeFilter(treePrefix); } } -async function setTreeFilter( - treePrefix: TreePrefix, - filterText: string -): Promise { +function setTreeFilter(treePrefix: TreePrefix, filterText: string): void { const normalizedFilterText = filterText.toLowerCase(); treeFilters.set(treePrefix, { filterText: normalizedFilterText, isActive: normalizedFilterText.length > 0, }); - await ext.context.workspaceState.update( + void ext.context.workspaceState.update( getFilterMementoKey(treePrefix), normalizedFilterText || undefined ); setFilterContextValue(treePrefix, normalizedFilterText.length > 0); } -async function clearTreeFilter(treePrefix: TreePrefix): Promise { +function clearTreeFilter(treePrefix: TreePrefix): void { treeFilters.delete(treePrefix); - await ext.context.workspaceState.update( + void ext.context.workspaceState.update( getFilterMementoKey(treePrefix), undefined ); @@ -163,15 +160,15 @@ async function filterTreeView( // Check if "Clear Filter" was selected if (selectedItem?.label === clearFilterLabel) { - await clearTreeFilter(treePrefix); + clearTreeFilter(treePrefix); context.telemetry.properties.action = "clearFilter"; } else if (value) { - await setTreeFilter(treePrefix, value); + setTreeFilter(treePrefix, value); context.telemetry.properties.action = "applyFilter"; context.telemetry.properties.filterLength = value.length.toString(); } else { - await clearTreeFilter(treePrefix); + clearTreeFilter(treePrefix); context.telemetry.properties.action = "clearFilter"; } @@ -265,7 +262,7 @@ export async function filterImagesTree(context: IActionContext): Promise { export async function clearContainersFilter( context: IActionContext ): Promise { - await clearTreeFilter("containers"); + clearTreeFilter("containers"); context.telemetry.properties.action = "clearFilter"; await refreshTreeView("containers"); } @@ -273,7 +270,7 @@ export async function clearContainersFilter( export async function clearImagesFilter( context: IActionContext ): Promise { - await clearTreeFilter("images"); + clearTreeFilter("images"); context.telemetry.properties.action = "clearFilter"; await refreshTreeView("images"); }