-
Notifications
You must be signed in to change notification settings - Fork 62
Feature/tree view search #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
bwateratmsft
merged 20 commits into
microsoft:main
from
FilipCondac:feature/tree-view-search
Dec 9, 2025
Merged
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
92d46b1
Add tree filtering logic
FilipCondac d880278
Add filter commands for containers, images, networks, registries, and…
FilipCondac 0ce103e
Implement logic to match filters
FilipCondac c8321a4
Add filter commands to package.json / package.nls.json
FilipCondac 4af3e0e
Fix codicon syntax error
FilipCondac ddde820
Remove filter support for networks,volumes,contexts and registries
FilipCondac b8c411d
Add $(search-stop) icon when active filter
FilipCondac fc0b3be
Implement fuzzy search fallback
FilipCondac ed2fe97
Merge upstream/main into feature/tree-view-search
FilipCondac 003af59
Delete FILTER_FEATURE_GUIDE.md
FilipCondac 9d82676
Delete FILTER_FEATURE_IMPLEMENTATION.md
FilipCondac 191440d
Revert package.json to upstream state
FilipCondac 813ffe5
Revert package.nls.json
FilipCondac cf706d1
Add package.json commands back
FilipCondac 1c37b01
Revert registerCommands to original state
FilipCondac d6854fa
Apply suggestions from code review
FilipCondac b8d4257
Use vscode.l10n.t()
FilipCondac 4d49553
Format to use 4 spaces per indent
FilipCondac ad71c6e
Merge remote-tracking branch 'upstream/main' into feature/tree-view-s…
FilipCondac 6fe43c9
Fix: Update treeFilters declaration for lint rules
FilipCondac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
FilipCondac marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See LICENSE.md in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import { IActionContext } from "@microsoft/vscode-azext-utils"; | ||
| import * as vscode from "vscode"; | ||
| import { ext } from "../extensionVariables"; | ||
| import { TreePrefix } from "../tree/TreePrefix"; | ||
|
|
||
| interface TreeFilterState { | ||
| filterText: string; | ||
| isActive: boolean; | ||
| } | ||
|
|
||
| const treeFilters: Map<TreePrefix, TreeFilterState> = new Map(); | ||
|
|
||
| // Only support filtering for containers and images | ||
| const contextKeys: Partial<Record<TreePrefix, string>> = { | ||
| containers: "vscode-containers:containersFiltered", | ||
| images: "vscode-containers:imagesFiltered", | ||
| }; | ||
|
|
||
| export function getTreeFilter(treePrefix: TreePrefix): TreeFilterState { | ||
| return treeFilters.get(treePrefix) || { filterText: "", isActive: false }; | ||
| } | ||
|
|
||
| export function setTreeFilter( | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| treePrefix: TreePrefix, | ||
| filterText: string | ||
| ): void { | ||
| treeFilters.set(treePrefix, { | ||
| filterText: filterText.toLowerCase(), | ||
| isActive: filterText.length > 0, | ||
| }); | ||
| setFilterContextValue(treePrefix, filterText.length > 0); | ||
| } | ||
|
|
||
| export function clearTreeFilter(treePrefix: TreePrefix): void { | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| treeFilters.set(treePrefix, { filterText: "", isActive: false }); | ||
| setFilterContextValue(treePrefix, false); | ||
| } | ||
|
|
||
| function setFilterContextValue(treePrefix: TreePrefix, value: boolean): void { | ||
| const contextKey = contextKeys[treePrefix]; | ||
| if (contextKey) { | ||
| void vscode.commands.executeCommand("setContext", contextKey, value); | ||
| } | ||
| } | ||
|
|
||
| export function setInitialFilterContextValues(): void { | ||
| for (const treePrefix of Object.keys(contextKeys) as TreePrefix[]) { | ||
| const filter = getTreeFilter(treePrefix); | ||
| setFilterContextValue(treePrefix, filter.isActive); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @param filterText The filter pattern (already lowercase) | ||
| * @param searchableText The text to search in (already lowercase) | ||
| */ | ||
| function fuzzyMatch(filterText: string, searchableText: string): boolean { | ||
| let filterIndex = 0; | ||
| let searchIndex = 0; | ||
|
|
||
| while ( | ||
| filterIndex < filterText.length && | ||
| searchIndex < searchableText.length | ||
| ) { | ||
| if (filterText[filterIndex] === searchableText[searchIndex]) { | ||
| filterIndex++; | ||
| } | ||
| searchIndex++; | ||
| } | ||
|
|
||
| return filterIndex === filterText.length; | ||
| } | ||
|
|
||
| export function shouldShowItem( | ||
| treePrefix: TreePrefix, | ||
| searchableText: string | ||
| ): boolean { | ||
| const filter = getTreeFilter(treePrefix); | ||
| if (!filter.isActive) { | ||
| return true; | ||
| } | ||
|
|
||
| const lowerSearchableText = searchableText.toLowerCase(); | ||
|
|
||
| if (lowerSearchableText.includes(filter.filterText)) { | ||
| return true; | ||
| } | ||
|
|
||
| return fuzzyMatch(filter.filterText, lowerSearchableText); | ||
| } | ||
|
|
||
| /** | ||
| * Command to filter a tree view | ||
| */ | ||
| export async function filterTreeView( | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| context: IActionContext, | ||
| treePrefix: TreePrefix | ||
| ): Promise<void> { | ||
| const currentFilter = getTreeFilter(treePrefix); | ||
|
|
||
| const quickPick = vscode.window.createQuickPick(); | ||
| quickPick.placeholder = `Filter ${treePrefix}... (Press Enter to apply, Esc to cancel)`; | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| quickPick.value = currentFilter.filterText; | ||
| quickPick.title = `Filter ${capitalize(treePrefix)}`; | ||
|
|
||
| if (currentFilter.isActive) { | ||
| quickPick.items = [ | ||
| { | ||
| label: "$(clear-all) Clear Filter", | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| description: `Currently filtering by: "${currentFilter.filterText}"`, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| quickPick.onDidAccept(() => { | ||
| const value = quickPick.value.trim(); | ||
| const selectedItem = quickPick.selectedItems[0]; | ||
|
|
||
| // Check if "Clear Filter" was selected | ||
| if (selectedItem?.label === "$(clear-all) Clear Filter") { | ||
| 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); | ||
| }); | ||
|
|
||
| quickPick.onDidHide(() => { | ||
| quickPick.dispose(); | ||
| }); | ||
|
|
||
| quickPick.show(); | ||
| } | ||
|
|
||
| /** | ||
| * Update the tree view title to show filter status | ||
| */ | ||
| export function updateTreeViewTitle(treePrefix: TreePrefix): void { | ||
|
FilipCondac marked this conversation as resolved.
Outdated
|
||
| const filter = getTreeFilter(treePrefix); | ||
| const treeView = getTreeViewForPrefix(treePrefix); | ||
|
|
||
| if (!treeView) { | ||
| return; | ||
| } | ||
|
|
||
| if (filter.isActive) { | ||
| treeView.description = `Filtered: "${filter.filterText}"`; | ||
| } else { | ||
| treeView.description = undefined; | ||
| } | ||
| } | ||
|
|
||
| function getTreeViewForPrefix( | ||
| treePrefix: TreePrefix | ||
| ): vscode.TreeView<unknown> | undefined { | ||
| switch (treePrefix) { | ||
| case "containers": | ||
| return ext.containersTreeView; | ||
| case "images": | ||
| return ext.imagesTreeView; | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| async function refreshTreeView(treePrefix: TreePrefix): Promise<void> { | ||
| updateTreeViewTitle(treePrefix); | ||
|
|
||
| // Get the root and refresh it | ||
| const root = getTreeRootForPrefix(treePrefix); | ||
| if (root) { | ||
| await root.refresh(undefined); | ||
| } | ||
| } | ||
|
|
||
| function getTreeRootForPrefix( | ||
| treePrefix: TreePrefix | ||
| ): { refresh(context: IActionContext): Promise<void> } | undefined { | ||
| switch (treePrefix) { | ||
| case "containers": | ||
| return ext.containersRoot; | ||
| case "images": | ||
| return ext.imagesRoot; | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| function capitalize(str: string): string { | ||
| return str.charAt(0).toUpperCase() + str.slice(1); | ||
| } | ||
|
|
||
| export async function filterContainersTree( | ||
| context: IActionContext | ||
| ): Promise<void> { | ||
| await filterTreeView(context, "containers"); | ||
| } | ||
|
|
||
| export async function filterImagesTree(context: IActionContext): Promise<void> { | ||
| await filterTreeView(context, "images"); | ||
| } | ||
|
|
||
| export async function clearContainersFilter( | ||
| context: IActionContext | ||
| ): Promise<void> { | ||
| clearTreeFilter("containers"); | ||
| context.telemetry.properties.action = "clearFilter"; | ||
| void refreshTreeView("containers"); | ||
| } | ||
|
|
||
| export async function clearImagesFilter( | ||
| context: IActionContext | ||
| ): Promise<void> { | ||
| clearTreeFilter("images"); | ||
| context.telemetry.properties.action = "clearFilter"; | ||
| void refreshTreeView("images"); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.