diff --git a/src/commands/filterTree.ts b/src/commands/filterTree.ts index fb5f0aa6..19d5d862 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,49 @@ 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 !== undefined) { + const normalizedFilter = persistedFilter.toLowerCase(); + treeFilters.set(treePrefix, { + filterText: normalizedFilter, + isActive: normalizedFilter.length > 0, + }); + } 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 +81,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); } } @@ -122,24 +158,31 @@ async function filterTreeView( } quickPick.onDidAccept(() => { - const value = quickPick.value.trim(); - const selectedItem = quickPick.selectedItems[0]; - - // Check if "Clear Filter" was selected - if (selectedItem?.label === clearFilterLabel) { - clearTreeFilter(treePrefix); - context.telemetry.properties.action = "clearFilter"; - } else if (value) { - setTreeFilter(treePrefix, value); - context.telemetry.properties.action = "applyFilter"; - context.telemetry.properties.filterLength = value.length.toString(); - } else { - clearTreeFilter(treePrefix); - context.telemetry.properties.action = "clearFilter"; - } - - quickPick.hide(); - void refreshTreeView(treePrefix); + 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(() => { @@ -223,15 +266,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"); } 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(); } } 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