diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 754c2020e434f..635cb940b4c00 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -108,6 +108,16 @@ export namespace SearchInWorkspaceCommands { export const REPLACE_ALL_RESULTS = Command.toDefaultLocalizedCommand({ id: 'search.action.replaceAll' }); + export const NEXT_MATCH = Command.toDefaultLocalizedCommand({ + id: 'search.action.nextMatchFindAction', + category: SEARCH_CATEGORY, + label: 'Focus Next Search Result', + }); + export const PREVIOUS_MATCH = Command.toDefaultLocalizedCommand({ + id: 'search.action.previousMatchFindAction', + category: SEARCH_CATEGORY, + label: 'Focus Previous Search Result', + }); } @injectable() @@ -262,6 +272,16 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut } }), }); + commands.registerCommand(SearchInWorkspaceCommands.NEXT_MATCH, { + isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()), + isVisible: () => this.withWidget(undefined, widget => widget.hasResultList()), + execute: () => this.withWidget(undefined, widget => widget.resultTreeWidget.focusNextResult()) + }); + commands.registerCommand(SearchInWorkspaceCommands.PREVIOUS_MATCH, { + isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()), + isVisible: () => this.withWidget(undefined, widget => widget.hasResultList()), + execute: () => this.withWidget(undefined, widget => widget.resultTreeWidget.focusPreviousResult()) + }); commands.registerCommand(SearchInWorkspaceCommands.COPY_ONE, { isEnabled: () => this.withWidget(undefined, widget => { const { selection } = this.selectionService; @@ -343,6 +363,16 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut keybinding: 'shift+alt+f', when: 'explorerResourceIsFolder' }); + keybindings.registerKeybindings({ + command: SearchInWorkspaceCommands.NEXT_MATCH.id, + keybinding: 'f4', + when: 'searchViewletFocus' + }); + keybindings.registerKeybinding({ + command: SearchInWorkspaceCommands.PREVIOUS_MATCH.id, + keybinding: 'shift+f4', + when: 'searchViewletFocus' + }); keybindings.registerKeybinding({ command: SearchInWorkspaceCommands.DISMISS_RESULT.id, keybinding: isOSX ? 'cmd+backspace' : 'del', diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx index b2e4bcda5d117..bf90fec0f95fd 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.tsx @@ -589,16 +589,114 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget { }); } + /** + * Selects and focuses the first result in the result tree. + */ focusFirstResult(): void { - if (SearchInWorkspaceRoot.is(this.model.root) && this.model.root.children.length > 0) { - const node = this.model.root.children[0]; - if (SelectableTreeNode.is(node)) { - this.node.focus(); - this.model.selectNode(node); + for (const rootFolderNode of this.resultTree.values()) { + for (const fileNode of rootFolderNode.children) { + for (const line of fileNode.children) { + if (SearchInWorkspaceResultLineNode.is(line)) { + this.model.selectNode(line); + this.node.focus(); + return; + } + } + } + } + } + + /** + * Selects and focuses the last result in the result tree. + */ + focusLastResult(): void { + for (const rootFolderNode of this.resultTree.values()) { + for (let i = rootFolderNode.children.length - 1; i >= 0; i--) { + const fileNode = rootFolderNode.children[i]; + for (let j = fileNode.children.length - 1; j >= 0; j--) { + const line = fileNode.children[j]; + if (SearchInWorkspaceResultLineNode.is(line)) { + this.model.selectNode(line); + this.node.focus(); + return; + } + } } } } + /** + * Selects and focuses the next result in the result tree. + * If there is no next result, the first result is focused. + */ + focusNextResult(): void { + const focusedResultLineNode = this.model.getFocusedNode(); + if (!focusedResultLineNode) { + return this.focusFirstResult(); + } + + let foundFocusedResultLineNode = false; + for (const rootFolderNode of this.resultTree.values()) { + for (const fileNode of rootFolderNode.children) { + for (const line of fileNode.children) { + if (SearchInWorkspaceResultLineNode.is(line)) { + if (foundFocusedResultLineNode) { + this.expansionService.expandNode(fileNode); + this.model.selectNode(line); + this.node.focus(); + return; + } + if (line === focusedResultLineNode) { + foundFocusedResultLineNode = true; + } + } + } + } + } + + // the focused node is not in the result tree + // or there is no next result + this.focusFirstResult(); + } + + /** + * Selects and focuses the previous result in the result tree. + * If there is no previous result, the last result is focused. + */ + focusPreviousResult(): void { + const focusedResultLineNode = this.model.getFocusedNode(); + if (!focusedResultLineNode) { + return this.focusLastResult(); + } + + let foundFocusedResultLineNode = false; + const rootFolderNodes = Array.from(this.resultTree.values()); + for (let i = rootFolderNodes.length - 1; i >= 0; i--) { + const rootFolderNode = rootFolderNodes[i]; + for (let j = rootFolderNode.children.length - 1; j >= 0; j--) { + const fileNode = rootFolderNode.children[j]; + for (let k = fileNode.children.length - 1; k >= 0; k--) { + const line = fileNode.children[k]; + if (SearchInWorkspaceResultLineNode.is(line)) { + if (foundFocusedResultLineNode) { + this.expansionService.expandNode(fileNode); + this.model.selectNode(line); + this.node.focus(); + return; + } + if (line === focusedResultLineNode) { + foundFocusedResultLineNode = true; + } + } + } + } + } + + // the focused node is not in the result tree + // or there is no previous result + this.focusLastResult(); + } + /** * Collapse the search-in-workspace file node * based on the preference value.