Skip to content
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

search-in-workspace: focus on next and previous search results #12703

Merged
merged 12 commits into from
Feb 8, 2024
74 changes: 74 additions & 0 deletions packages/core/src/browser/tree/tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,41 @@ export interface TreeModel extends Tree, TreeSelectionService, TreeExpansionServ
*/
navigateBackward(): Promise<void>;

/**
* Selects the previous tree node, regardless of its selection or visibility state.
*/
selectPrev(): void;

/**
* Selects the previous node relatively to the currently selected one. This method takes the expansion state of the tree into consideration.
*/
selectPrevNode(type?: TreeSelection.SelectionType): void;

/**
* Returns the previous tree node, regardless of its selection or visibility state.
*/
getPrevNode(node?: TreeNode): TreeNode | undefined;

/**
* Returns the previous selectable tree node.
*/
getPrevSelectableNode(node?: TreeNode): SelectableTreeNode | undefined;

/**
* Selects the next tree node, regardless of its selection or visibility state.
*/
selectNext(): void;

/**
* Selects the next node relatively to the currently selected one. This method takes the expansion state of the tree into consideration.
*/
selectNextNode(type?: TreeSelection.SelectionType): void;

/**
* Returns the next tree node, regardless of its selection or visibility state.
*/
getNextNode(node?: TreeNode): TreeNode | undefined;

/**
* Returns the next selectable tree node.
*/
Expand Down Expand Up @@ -294,13 +314,23 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
}
}

selectPrev(): void {
const node = this.getPrevNode();
this.selectNodeIfSelectable(node);
}

selectPrevNode(type: TreeSelection.SelectionType = TreeSelection.SelectionType.DEFAULT): void {
const node = this.getPrevSelectableNode();
if (node) {
this.addSelection({ node, type });
}
}

getPrevNode(node: TreeNode | undefined = this.getFocusedNode()): TreeNode | undefined {
const iterator = this.createBackwardTreeIterator(node);
return iterator && this.doGetNode(iterator);
}

getPrevSelectableNode(node: TreeNode | undefined = this.getFocusedNode()): SelectableTreeNode | undefined {
if (!node) {
return this.getNextSelectableNode(this.root);
Expand All @@ -309,18 +339,40 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
return iterator && this.doGetNextNode(iterator, this.isVisibleSelectableNode.bind(this));
}

selectNext(): void {
const node = this.getNextNode();
this.selectNodeIfSelectable(node);
}

selectNextNode(type: TreeSelection.SelectionType = TreeSelection.SelectionType.DEFAULT): void {
const node = this.getNextSelectableNode();
if (node) {
this.addSelection({ node, type });
}
}

getNextNode(node: TreeNode | undefined = this.getFocusedNode()): TreeNode | undefined {
const iterator = this.createTreeIterator(node);
return iterator && this.doGetNode(iterator);
}

getNextSelectableNode(node: TreeNode | undefined = this.getFocusedNode() ?? this.root): SelectableTreeNode | undefined {
const iterator = this.createIterator(node);
return iterator && this.doGetNextNode(iterator, this.isVisibleSelectableNode.bind(this));
}

protected selectNodeIfSelectable(node: TreeNode | undefined): void {
if (SelectableTreeNode.is(node)) {
this.addSelection(node);
}
}

protected doGetNode(iterator: TreeIterator): TreeNode | undefined {
iterator.next();
const result = iterator.next();
return result.done ? undefined : result.value;
}

protected doGetNextNode<T extends TreeNode>(iterator: TreeIterator, criterion: (node: TreeNode) => node is T): T | undefined {
// Skip the first item. // TODO: clean this up, and skip the first item in a different way without loading everything.
iterator.next();
Expand All @@ -338,6 +390,17 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
return SelectableTreeNode.isVisible(node);
}

protected createBackwardTreeIterator(node: TreeNode | undefined): TreeIterator | undefined {
const { filteredNodes } = this.treeSearch;
if (filteredNodes.length === 0) {
return node ? new BottomUpTreeIterator(node!, { pruneCollapsed: false }) : undefined;
}
if (node && filteredNodes.indexOf(node) === -1) {
return undefined;
}
return Iterators.cycle(filteredNodes.slice().reverse(), node);
}

protected createBackwardIterator(node: TreeNode | undefined): TreeIterator | undefined {
const { filteredNodes } = this.treeSearch;
if (filteredNodes.length === 0) {
Expand All @@ -349,6 +412,17 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
return Iterators.cycle(filteredNodes.slice().reverse(), node);
}

protected createTreeIterator(node: TreeNode | undefined): TreeIterator | undefined {
const { filteredNodes } = this.treeSearch;
if (filteredNodes.length === 0) {
return node && new TopDownTreeIterator(node, { pruneCollapsed: false });
}
if (node && filteredNodes.indexOf(node) === -1) {
return undefined;
}
return Iterators.cycle(filteredNodes, node);
}

protected createIterator(node: TreeNode | undefined): TreeIterator | undefined {
const { filteredNodes } = this.treeSearch;
if (filteredNodes.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ export namespace SearchInWorkspaceCommands {
category: SEARCH_CATEGORY,
label: 'Find in Folder...'
});
export const FOCUS_NEXT_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.focusNextSearchResult',
category: SEARCH_CATEGORY,
label: 'Focus Next Search Result'
});
export const FOCUS_PREV_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.focusPreviousSearchResult',
category: SEARCH_CATEGORY,
label: 'Focus Previous Search Result'
});
export const REFRESH_RESULTS = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.refresh',
category: SEARCH_CATEGORY,
Expand Down Expand Up @@ -169,6 +179,22 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut
}
});

commands.registerCommand(SearchInWorkspaceCommands.FOCUS_NEXT_RESULT, {
isEnabled: () => this.tryGetWidget()?.hasResultList() ?? false,
vladarama marked this conversation as resolved.
Show resolved Hide resolved
execute: async () => {
const widget = await this.openView({ reveal: true });
widget.resultTreeWidget.selectNextResult();
}
});

commands.registerCommand(SearchInWorkspaceCommands.FOCUS_PREV_RESULT, {
isEnabled: () => this.tryGetWidget()?.hasResultList() ?? false,
execute: async () => {
const widget = await this.openView({ reveal: true });
widget.resultTreeWidget.selectPreviousResult();
}
});

commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER, this.newMultiUriAwareCommandHandler({
execute: async uris => {
const resources: string[] = [];
Expand Down Expand Up @@ -343,6 +369,14 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut
keybinding: 'shift+alt+f',
when: 'explorerResourceIsFolder'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.FOCUS_NEXT_RESULT.id,
keybinding: 'f4',
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.FOCUS_PREV_RESULT.id,
keybinding: 'shift+f4',
});
vladarama marked this conversation as resolved.
Show resolved Hide resolved
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.DISMISS_RESULT.id,
keybinding: isOSX ? 'cmd+backspace' : 'del',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,79 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
return true;
}

selectNextResult(): void {
if (!this.model.getFocusedNode()) {
return this.selectFirstResult();
}
let foundNextResult = false;
while (!foundNextResult) {
const nextNode = this.model.getNextNode();
if (!nextNode) {
return this.selectFirstResult();
} else if (SearchInWorkspaceResultLineNode.is(nextNode)) {
foundNextResult = true;
this.selectExpandOpenResultNode(nextNode);
} else {
this.model.selectNext();
}
}
}

selectPreviousResult(): void {
if (!this.model.getFocusedNode()) {
return this.selectLastResult();
}
let foundSelectedNode = false;
while (!foundSelectedNode) {
const prevNode = this.model.getPrevNode();
if (!prevNode) {
return this.selectLastResult();
} else if (SearchInWorkspaceResultLineNode.is(prevNode)) {
foundSelectedNode = true;
this.selectExpandOpenResultNode(prevNode);
} else if (prevNode.id === 'ResultTree') {
return this.selectLastResult();
} else {
this.model.selectPrev();
}
}
}

protected selectExpandOpenResultNode(node: SearchInWorkspaceResultLineNode): void {
this.model.expandNode(node.parent.parent);
this.model.expandNode(node.parent);
this.model.selectNode(node);
this.model.openNode(node);
}

protected selectFirstResult(): void {
for (const rootFolder of this.resultTree.values()) {
for (const file of rootFolder.children) {
for (const result of file.children) {
if (SelectableTreeNode.is(result)) {
return this.selectExpandOpenResultNode(result);
}
}
}
}
}

protected selectLastResult(): void {
const rootFolders = Array.from(this.resultTree.values());
for (let i = rootFolders.length - 1; i >= 0; i--) {
const rootFolder = rootFolders[i];
for (let j = rootFolder.children.length - 1; j >= 0; j--) {
const file = rootFolder.children[j];
for (let k = file.children.length - 1; k >= 0; k--) {
const result = file.children[k];
if (SelectableTreeNode.is(result)) {
return this.selectExpandOpenResultNode(result);
}
}
}
}
}

/**
* Find matches for the given editor.
* @param searchTerm the search term.
Expand Down