-
Notifications
You must be signed in to change notification settings - Fork 30
feat: Add search filter to sidebar #562
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
bab6c21
3e340bf
9dd25a8
8f103e5
2d186c7
448d947
a5c0ceb
de2dd41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -684,6 +684,53 @@ export class Commands { | |
}); | ||
} | ||
|
||
/** | ||
* Search/filter workspaces in the All Workspaces view. | ||
* This method will be called from the view title menu. | ||
*/ | ||
public async searchWorkspaces(): Promise<void> { | ||
const quickPick = vscode.window.createQuickPick(); | ||
quickPick.placeholder = | ||
"Type to search workspaces by name, owner, template, status, or agent"; | ||
quickPick.title = "Search All Workspaces"; | ||
quickPick.value = ""; | ||
|
||
// Get current search filter to show in the input | ||
const currentFilter = (await vscode.commands.executeCommand( | ||
"coder.getWorkspaceSearchFilter", | ||
)) as string; | ||
if (currentFilter) { | ||
quickPick.value = currentFilter; | ||
} | ||
|
||
quickPick.ignoreFocusOut = true; // Keep open when clicking elsewhere | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like an odd thing to set. can you explain your rationale? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, here was my thought process on user behavior - When a user clicks outside the Quickpick (like on the workspace list or elsewhere in VS Code), the quickpick automatically closes
With the current approach it solves the above scenarios. The context is still preserved in state (private searchFilter) so the filtered results will not change if the input closes - its more so just a means for the user to not have to click on the search button again if they are not satisfied or have not completed their search |
||
quickPick.canSelectMany = false; // Don't show selection list | ||
|
||
quickPick.onDidChangeValue((value) => { | ||
// Update the search filter in real-time as user types | ||
vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", value); | ||
}); | ||
|
||
quickPick.onDidAccept(() => { | ||
// When user presses Enter, close the search | ||
quickPick.hide(); | ||
}); | ||
|
||
quickPick.onDidHide(() => { | ||
// Don't clear the search when closed - keep the filter active | ||
quickPick.dispose(); | ||
}); | ||
|
||
quickPick.show(); | ||
} | ||
|
||
/** | ||
* Clear the workspace search filter. | ||
*/ | ||
public clearWorkspaceSearch(): void { | ||
vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", ""); | ||
} | ||
|
||
/** | ||
* Return agents from the workspace. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ export class WorkspaceProvider | |
private timeout: NodeJS.Timeout | undefined; | ||
private fetching = false; | ||
private visible = false; | ||
private searchFilter = ""; | ||
|
||
constructor( | ||
private readonly getWorkspacesQuery: WorkspaceQuery, | ||
|
@@ -52,6 +53,15 @@ export class WorkspaceProvider | |
// No initialization. | ||
} | ||
|
||
setSearchFilter(filter: string) { | ||
this.searchFilter = filter; | ||
this.refresh(undefined); | ||
} | ||
|
||
getSearchFilter(): string { | ||
return this.searchFilter; | ||
} | ||
|
||
// fetchAndRefresh fetches new workspaces, re-renders the entire tree, then | ||
// keeps refreshing (if a timer length was provided) as long as the user is | ||
// still logged in and no errors were encountered fetching workspaces. | ||
|
@@ -300,7 +310,90 @@ export class WorkspaceProvider | |
|
||
return Promise.resolve([]); | ||
} | ||
return Promise.resolve(this.workspaces || []); | ||
|
||
// Filter workspaces based on search term | ||
let filteredWorkspaces = this.workspaces || []; | ||
const trimmedFilter = this.searchFilter.trim(); | ||
if (trimmedFilter) { | ||
const searchTerm = trimmedFilter.toLowerCase(); | ||
filteredWorkspaces = filteredWorkspaces.filter((workspace) => | ||
this.matchesSearchTerm(workspace, searchTerm), | ||
); | ||
} | ||
|
||
return Promise.resolve(filteredWorkspaces); | ||
} | ||
|
||
/** | ||
* Check if a workspace matches the given search term using smart search logic. | ||
* Prioritizes exact word matches over substring matches. | ||
*/ | ||
private matchesSearchTerm( | ||
workspace: WorkspaceTreeItem, | ||
searchTerm: string, | ||
): boolean { | ||
const workspaceName = workspace.workspace.name.toLowerCase(); | ||
const ownerName = workspace.workspace.owner_name.toLowerCase(); | ||
const templateName = ( | ||
workspace.workspace.template_display_name || | ||
workspace.workspace.template_name || | ||
"" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this first |
||
).toLowerCase(); | ||
const status = workspace.workspace.latest_build.status.toLowerCase(); | ||
|
||
// Check if any agent names match the search term | ||
const agents = extractAgents(workspace.workspace.latest_build.resources); | ||
const agentNames = agents.map((agent) => agent.name.toLowerCase()); | ||
const hasMatchingAgent = agentNames.some((agentName) => | ||
agentName.includes(searchTerm), | ||
); | ||
|
||
// Check if any agent metadata contains the search term | ||
const hasMatchingMetadata = agents.some((agent) => { | ||
const watcher = this.agentWatchers[agent.id]; | ||
if (watcher?.metadata) { | ||
return watcher.metadata.some((metadata) => { | ||
const metadataStr = JSON.stringify(metadata).toLowerCase(); | ||
yelnatscoding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return metadataStr.includes(searchTerm); | ||
}); | ||
} | ||
return false; | ||
}); | ||
|
||
// Smart search: Try exact word match first, then fall back to substring | ||
const searchWords = searchTerm | ||
.split(/\s+/) | ||
.filter((word) => word.length > 0); | ||
const allText = [ | ||
workspaceName, | ||
ownerName, | ||
templateName, | ||
status, | ||
...agentNames, | ||
].join(" "); | ||
|
||
// Check for exact word matches (higher priority) | ||
const hasExactWordMatch = | ||
searchWords.length > 0 && | ||
searchWords.some((word) => { | ||
// Escape special regex characters to prevent injection | ||
yelnatscoding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
const wordBoundaryRegex = new RegExp(`\\b${escapedWord}\\b`, "i"); | ||
return wordBoundaryRegex.test(allText); | ||
}); | ||
yelnatscoding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Check for substring matches (lower priority) - only if no exact word match | ||
const hasSubstringMatch = | ||
yelnatscoding marked this conversation as resolved.
Show resolved
Hide resolved
|
||
!hasExactWordMatch && | ||
(workspaceName.includes(searchTerm) || | ||
ownerName.includes(searchTerm) || | ||
templateName.includes(searchTerm) || | ||
status.includes(searchTerm) || | ||
hasMatchingAgent || | ||
hasMatchingMetadata); | ||
|
||
// Return true if either exact word match or substring match | ||
return hasExactWordMatch || hasSubstringMatch; | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels a little weird to put this in the middle
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where do you suggest we move it to? or should we remove it entirely?