Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@
"command": "vscode-containers.containers.select",
"when": "never"
},
{
"command": "vscode-containers.containers.clearFilter",
"when": "never"
},
{
"command": "vscode-containers.images.clearFilter",
"when": "never"
},
{
"command": "vscode-containers.registries.reconnectRegistry",
"when": "never"
Expand Down Expand Up @@ -232,6 +240,16 @@
"when": "view == vscode-containers.views.containers",
"group": "navigation@1"
},
{
"command": "vscode-containers.containers.filter",
"when": "view == vscode-containers.views.containers && !vscode-containers:containersFiltered",
"group": "navigation@2"
},
{
"command": "vscode-containers.containers.clearFilter",
"when": "view == vscode-containers.views.containers && vscode-containers:containersFiltered",
"group": "navigation@2"
},
{
"command": "vscode-containers.chooseContainerRuntime",
"when": "view == vscode-containers.views.containers",
Expand Down Expand Up @@ -272,6 +290,16 @@
"when": "view == vscode-containers.views.images",
"group": "navigation@2"
},
{
"command": "vscode-containers.images.filter",
"when": "view == vscode-containers.views.images && !vscode-containers:imagesFiltered",
"group": "navigation@3"
},
{
"command": "vscode-containers.images.clearFilter",
"when": "view == vscode-containers.views.images && vscode-containers:imagesFiltered",
"group": "navigation@3"
},
{
"command": "vscode-containers.images.showDangling",
"when": "view == vscode-containers.views.images && !vscode-containers:danglingShown",
Expand Down Expand Up @@ -2538,6 +2566,18 @@
"title": "%vscode-containers.commands.containers.composeGroup.down%",
"category": "%vscode-containers.commands.category.containers%"
},
{
"command": "vscode-containers.containers.filter",
"title": "%vscode-containers.commands.containers.filter%",
"category": "%vscode-containers.commands.category.containers%",
"icon": "$(search)"
},
{
"command": "vscode-containers.containers.clearFilter",
"title": "%vscode-containers.commands.containers.clearFilter%",
"category": "%vscode-containers.commands.category.containers%",
"icon": "$(search-stop)"
},
{
"command": "vscode-containers.debugging.initializeForDebugging",
"title": "%vscode-containers.commands.debugging.initializeForDebugging%",
Expand Down Expand Up @@ -2628,6 +2668,18 @@
"title": "%vscode-containers.commands.images.copyFullTag%",
"category": "%vscode-containers.commands.category.images%"
},
{
"command": "vscode-containers.images.filter",
"title": "%vscode-containers.commands.images.filter%",
"category": "%vscode-containers.commands.category.images%",
"icon": "$(search)"
},
{
"command": "vscode-containers.images.clearFilter",
"title": "%vscode-containers.commands.images.clearFilter%",
"category": "%vscode-containers.commands.category.images%",
"icon": "$(search-stop)"
},
{
"command": "vscode-containers.networks.configureExplorer",
"title": "%vscode-containers.commands.networks.configureExplorer%",
Expand Down
4 changes: 4 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@
"vscode-containers.commands.containers.attachShell": "Attach Shell",
"vscode-containers.commands.containers.browse": "Open in Browser",
"vscode-containers.commands.containers.configureExplorer": "Configure Explorer...",
"vscode-containers.commands.containers.filter": "Filter...",
"vscode-containers.commands.containers.clearFilter": "Clear Filter",
"vscode-containers.commands.containers.downloadFile": "Download...",
"vscode-containers.commands.containers.inspect": "Inspect",
"vscode-containers.commands.containers.openFile": "Open",
Expand All @@ -244,6 +246,8 @@
"vscode-containers.commands.chooseContainerRuntime": "Choose container runtime...",
"vscode-containers.commands.images.build": "Build Image...",
"vscode-containers.commands.images.configureExplorer": "Configure Explorer...",
"vscode-containers.commands.images.filter": "Filter...",
"vscode-containers.commands.images.clearFilter": "Clear Filter",
"vscode-containers.commands.images.inspect": "Inspect",
"vscode-containers.commands.images.showDangling": "Show dangling images",
"vscode-containers.commands.images.hideDangling": "Hide dangling images",
Expand Down
230 changes: 230 additions & 0 deletions src/commands/filterTree.ts
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(
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 {
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(
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)`;
quickPick.value = currentFilter.filterText;
quickPick.title = `Filter ${capitalize(treePrefix)}`;

if (currentFilter.isActive) {
quickPick.items = [
{
label: "$(clear-all) Clear Filter",
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 {
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");
}
6 changes: 6 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { configureDockerContextsExplorer, dockerContextsHelp } from "./context/D
import { inspectDockerContext } from "./context/inspectDockerContext";
import { removeDockerContext } from "./context/removeDockerContext";
import { useDockerContext } from "./context/useDockerContext";
import { clearContainersFilter, clearImagesFilter, filterContainersTree, filterImagesTree, setInitialFilterContextValues } from "./filterTree";
import { help, reportIssue } from "./help";
import { buildImage } from "./images/buildImage";
import { configureImagesExplorer } from "./images/configureImagesExplorer";
Expand Down Expand Up @@ -129,6 +130,8 @@ export function registerCommands(): void {
registerCommand('vscode-containers.containers.downloadFile', downloadContainerFile);
registerCommand('vscode-containers.containers.inspect', inspectContainer);
registerCommand('vscode-containers.containers.configureExplorer', configureContainersExplorer);
registerCommand('vscode-containers.containers.filter', filterContainersTree);
registerCommand('vscode-containers.containers.clearFilter', clearContainersFilter);
registerCommand('vscode-containers.containers.openFile', openContainerFile);
registerCommand('vscode-containers.containers.prune', pruneContainers);
registerCommand('vscode-containers.containers.remove', removeContainer);
Expand All @@ -147,11 +150,14 @@ export function registerCommands(): void {

registerWorkspaceCommand('vscode-containers.images.build', buildImage);
registerCommand('vscode-containers.images.configureExplorer', configureImagesExplorer);
registerCommand('vscode-containers.images.filter', filterImagesTree);
registerCommand('vscode-containers.images.clearFilter', clearImagesFilter);
registerCommand('vscode-containers.images.inspect', inspectImage);
registerCommand('vscode-containers.images.prune', pruneImages);
registerCommand('vscode-containers.images.showDangling', showDanglingImages);
registerCommand('vscode-containers.images.hideDangling', hideDanglingImages);
setInitialDanglingContextValue();
setInitialFilterContextValues();
registerWorkspaceCommand('vscode-containers.images.pull', pullImage);
registerWorkspaceCommand('vscode-containers.images.push', pushImage);
registerCommand('vscode-containers.images.remove', removeImage);
Expand Down
Loading