diff --git a/locales/en-US/browser/browser/zen-general.ftl b/locales/en-US/browser/browser/zen-general.ftl index d1229b04f..0ec7200b7 100644 --- a/locales/en-US/browser/browser/zen-general.ftl +++ b/locales/en-US/browser/browser/zen-general.ftl @@ -79,6 +79,7 @@ zen-icons-picker-svg = .label = Icons urlbar-search-mode-zen_actions = Actions +urlbar-search-mode-workspaces = { zen-panel-ui-workspaces-text } zen-site-data-settings = Settings zen-generic-manage = Manage diff --git a/prefs/zen/zen-urlbar.yaml b/prefs/zen/zen-urlbar.yaml index a39a77e24..fefa24e40 100644 --- a/prefs/zen/zen-urlbar.yaml +++ b/prefs/zen/zen-urlbar.yaml @@ -39,3 +39,6 @@ - name: zen.urlbar.suggestions.quick-actions value: true + +- name: browser.urlbar.shortcuts.workspaces + value: true \ No newline at end of file diff --git a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch index 1a27bcc25..9b41ab880 100644 --- a/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarPrefs-sys-mjs.patch @@ -1,12 +1,21 @@ diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs -index 2d21248256c6c2bfb8dac958133c10e3251ef564..6645211ef09518b41cb737e3186fbd0162ecf700 100644 +index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs -@@ -799,6 +799,7 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) { +@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ + ["shortcuts.tabs", true], + ["shortcuts.history", true], + ["shortcuts.actions", true], ++ ["shortcuts.workspaces", true], + + // Boolean to determine if the providers defined in `exposureResults` + // should be displayed in search results. This can be set by a +@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) { */ let rootGroup = { children: [ + { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_ACTION }] }, ++ { children: [{ group: lazy.UrlbarUtils.RESULT_GROUP.ZEN_WORKSPACE }] }, // heuristic { maxResultCount: 1, diff --git a/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch index 1993cacce..92a550307 100644 --- a/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarProvidersManager-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs -index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a44ea79fa 100644 +index 08455d8d5da233639ccebc0e77c0810fb4f674c3..da8092b561c3dd8864e57f5a52a1a643db29ace1 100644 --- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs +++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs @@ -913,6 +913,7 @@ export class Query { @@ -10,3 +10,26 @@ index 08455d8d5da233639ccebc0e77c0810fb4f674c3..78d0e875978b568b79646489c28b125a (!this.context.trimmedSearchString || (!this.context.searchMode.engineName && !result.autofill)) ) { +@@ -1043,6 +1044,7 @@ function updateSourcesIfEmpty(context) { + lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE, + lazy.UrlbarTokenizer.TYPE.RESTRICT_URL, + lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION, ++ lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE, + ].includes(t.type) + ); + +@@ -1100,6 +1102,14 @@ function updateSourcesIfEmpty(context) { + acceptedSources.push(source); + } + break; ++ case lazy.UrlbarUtils.RESULT_SOURCE.WORKSPACES: ++ if ( ++ restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_WORKSPACE || ++ !restrictTokenType ++ ) { ++ acceptedSources.push(source); ++ } ++ break; + case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL: + case lazy.UrlbarUtils.RESULT_SOURCE.ADDON: + default: diff --git a/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch b/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch new file mode 100644 index 000000000..c195ba7d9 --- /dev/null +++ b/src/browser/components/urlbar/UrlbarTokenizer-sys-mjs.patch @@ -0,0 +1,28 @@ +diff --git a/browser/components/urlbar/UrlbarTokenizer.sys.mjs b/browser/components/urlbar/UrlbarTokenizer.sys.mjs +index d4af0ee5138a69139b94d898fb07e2345172f025..f750aae3f9f0a849ca009784510575b2b7119e6d 100644 +--- a/browser/components/urlbar/UrlbarTokenizer.sys.mjs ++++ b/browser/components/urlbar/UrlbarTokenizer.sys.mjs +@@ -66,6 +66,7 @@ export var UrlbarTokenizer = { + // `looksLikeOrigin()` returned `LOOKS_LIKE_ORIGIN.OTHER` for this token. It + // may or may not be an origin. + POSSIBLE_ORIGIN_BUT_SEARCH_ALLOWED: 12, ++ RESTRICT_WORKSPACE: 13, + }), + + // The special characters below can be typed into the urlbar to restrict +@@ -83,6 +84,7 @@ export var UrlbarTokenizer = { + TITLE: "#", + URL: "$", + ACTION: ">", ++ WORKSPACE: "`", + }), + + // The keys of characters in RESTRICT that will enter search mode. +@@ -97,6 +99,7 @@ export var UrlbarTokenizer = { + if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) { + keys.push(this.RESTRICT.ACTION); + } ++ keys.push(this.RESTRICT.WORKSPACE); + return new Set(keys); + }, + diff --git a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch index 4082b9b91..f03d334fb 100644 --- a/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarUtils-sys-mjs.patch @@ -1,29 +1,50 @@ diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs -index 64afd613f454edd7786fcc1e2f307a582e4d5f51..b4af9cc2fbddba2c5229e8ffee7b9c0c06c3e1d0 100644 +index 64afd613f454edd7786fcc1e2f307a582e4d5f51..50e2dd129d4f2e8f0b07e29639d660cd08ee7318 100644 --- a/browser/components/urlbar/UrlbarUtils.sys.mjs +++ b/browser/components/urlbar/UrlbarUtils.sys.mjs -@@ -85,6 +85,7 @@ export var UrlbarUtils = { +@@ -85,6 +85,8 @@ export var UrlbarUtils = { RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword", SUGGESTED_INDEX: "suggestedIndex", TAIL_SUGGESTION: "tailSuggestion", + ZEN_ACTION: "zenAction", ++ ZEN_WORKSPACE: "zenWorkspace", }), // Defines provider types. -@@ -146,6 +147,7 @@ export var UrlbarUtils = { +@@ -146,6 +148,8 @@ export var UrlbarUtils = { OTHER_NETWORK: 6, ADDON: 7, ACTIONS: 8, + ZEN_ACTIONS: 9, ++ WORKSPACES: 10, }), // Per-result exposure telemetry. -@@ -587,6 +589,8 @@ export var UrlbarUtils = { +@@ -295,6 +299,14 @@ export var UrlbarUtils = { + telemetryLabel: "actions", + uiLabel: "urlbar-searchmode-actions", + }, ++ { ++ source: this.RESULT_SOURCE.WORKSPACES, ++ restrict: lazy.UrlbarTokenizer.RESTRICT.WORKSPACE, ++ icon: "chrome://browser/skin/zen-icons/selectable/layers.svg", ++ pref: "shortcuts.workspaces", ++ telemetryLabel: "workspaces", ++ uiLabel: "urlbar-searchmode-workspaces", ++ }, + ]); + }, + +@@ -587,6 +599,12 @@ export var UrlbarUtils = { return this.RESULT_GROUP.HEURISTIC_FALLBACK; case "UrlbarProviderHistoryUrlHeuristic": return this.RESULT_GROUP.HEURISTIC_HISTORY_URL; + case "ZenUrlbarProviderGlobalActions": -+ return this.RESULT_GROUP.ZEN_ACTION; ++ if (result.source == this.RESULT_SOURCE.WORKSPACES) { ++ return this.RESULT_GROUP.ZEN_WORKSPACE; ++ } else { ++ return this.RESULT_GROUP.ZEN_ACTION; ++ } case "UrlbarProviderOmnibox": return this.RESULT_GROUP.HEURISTIC_OMNIBOX; case "UrlbarProviderRestrictKeywordsAutofill": diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index 805d66d95..0e949942f 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -796,7 +796,8 @@ --fp-enabled: 1; } -#alltabs-button { +#alltabs-button, +#urlbar-engine-one-off-item-workspaces { list-style-image: url("chrome://browser/skin/tabs.svg") !important; } diff --git a/src/zen/tests/ub-actions/browser.toml b/src/zen/tests/ub-actions/browser.toml index b0915f7ac..c0e864bd5 100644 --- a/src/zen/tests/ub-actions/browser.toml +++ b/src/zen/tests/ub-actions/browser.toml @@ -5,3 +5,4 @@ [DEFAULT] ["browser_ub_actions_search.js"] +["browser_workspace_restrict_search.js"] diff --git a/src/zen/tests/ub-actions/browser_workspace_restrict_search.js b/src/zen/tests/ub-actions/browser_workspace_restrict_search.js new file mode 100644 index 000000000..27adf48eb --- /dev/null +++ b/src/zen/tests/ub-actions/browser_workspace_restrict_search.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", + UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", +}); + +UrlbarTestUtils.init(this); + +add_task(async function test_Workspace_Search_OneOff_Pref() { + const oneOffSearchButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + + async function openPopupAndWaitForRebuild(value = "") { + oneOffSearchButtons.invalidateCache(); + const rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value, + }); + await rebuildPromise; + } + + function getWorkspaceShortcut() { + return [...oneOffSearchButtons.localButtons].find( + button => button.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES + ); + } + + try { + await SpecialPowers.pushPrefEnv({ + set: [["zen.urlbar.hide-one-offs", false]], + }); + + await openPopupAndWaitForRebuild(); + ok( + getWorkspaceShortcut(), + "The workspace shortcut should be visible when the pref is enabled" + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.shortcuts.workspaces", false]], + }); + + try { + await openPopupAndWaitForRebuild(); + ok( + !getWorkspaceShortcut(), + "The workspace shortcut should be hidden when the pref is disabled" + ); + await UrlbarTestUtils.promisePopupClose(window); + } finally { + await SpecialPowers.popPrefEnv(); + } + } finally { + await SpecialPowers.popPrefEnv(); + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window); + } + } +}); + +add_task(async function test_Workspace_Restrict_Search() { + const originalWorkspaceId = gZenWorkspaces.activeWorkspace; + const workspaceName = "zen-urlbar-workspace-proof-617db8"; + + await gZenWorkspaces.createAndSaveWorkspace(workspaceName); + + const createdWorkspace = gZenWorkspaces + .getWorkspaces() + .find(workspace => workspace.name == workspaceName); + ok(createdWorkspace, "Created the workspace used by the urlbar test"); + + registerCleanupFunction(async function () { + if (UrlbarTestUtils.isPopupOpen(window)) { + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + } + if (gZenWorkspaces.activeWorkspace != originalWorkspaceId) { + await gZenWorkspaces.changeWorkspace(originalWorkspaceId); + } + if ( + gZenWorkspaces + .getWorkspaces() + .some(workspace => workspace.uuid == createdWorkspace.uuid) + ) { + await gZenWorkspaces.removeWorkspace(createdWorkspace.uuid); + } + }); + + await gZenWorkspaces.changeWorkspace(originalWorkspaceId); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "` " + workspaceName, + }); + // Wait for the second search started when the typed token enters search mode. + await UrlbarTestUtils.promiseSearchComplete(window); + ok(gURLBar.searchMode, "The urlbar should enter search mode"); + Assert.equal( + gURLBar.searchMode.source, + UrlbarUtils.RESULT_SOURCE.WORKSPACES, + "The typed token should enter workspace search mode" + ); + Assert.equal( + gURLBar.searchMode.entry, + "typed", + "The workspace search mode should be entered by typing the token" + ); + Assert.equal(gURLBar.value, workspaceName, "The token should be stripped"); + + const resultCount = UrlbarTestUtils.getResultCount(window); + ok(resultCount > 0, "Should show at least one workspace result"); + + const resultDetails = []; + for (let i = 0; i < resultCount; i++) { + resultDetails.push(await UrlbarTestUtils.getDetailsOfResultAt(window, i)); + } + + ok( + resultDetails.every( + ({ result, source }) => + source == UrlbarUtils.RESULT_SOURCE.WORKSPACES && + result.providerName == "ZenUrlbarProviderGlobalActions" + ), + "Typing the workspace token should limit results to workspace actions" + ); + Assert.equal( + resultDetails[0].result.payload.prettyName, + workspaceName, + "The matching workspace should be shown first" + ); +}); diff --git a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs index 8adf50107..5fb7d1c4c 100644 --- a/src/zen/urlbar/ZenUBActionsProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBActionsProvider.sys.mjs @@ -155,6 +155,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { */ async isActive(queryContext) { return ( + queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES || queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS || (lazy.enabledPref && @@ -241,10 +242,13 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { * * @param {string} query The user's search query. * @param {boolean} isPrefixed Whether the query is prefixed. + * @param {boolean} isWorkspaceSearch Whether this is a workspace search query */ - async #findMatchingActions(query, isPrefixed) { + async #findMatchingActions(query, isPrefixed, isWorkspaceSearch) { const window = lazy.BrowserWindowTracker.getTopWindow(); - const actions = await this.#getAvailableActions(window); + const actions = isWorkspaceSearch + ? this.#getWorkspaceActions(window) + : await this.#getAvailableActions(window); let results = []; for (let action of actions) { if (isPrefixed && query.length < 1) { @@ -341,13 +345,21 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { async startQuery(queryContext, addCallback) { const query = queryContext.trimmedLowerCaseSearchString; + const isWorkspaceSearch = + queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.WORKSPACES; const isPrefixed = + isWorkspaceSearch || queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS; + if (!query && !isPrefixed) { return; } - const actionsResults = await this.#findMatchingActions(query, isPrefixed); + const actionsResults = await this.#findMatchingActions( + query, + isPrefixed, + isWorkspaceSearch + ); if (!actionsResults.length) { return; } @@ -361,9 +373,12 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { zenCommand: action.command, dynamicType: DYNAMIC_TYPE_NAME, zenAction: true, - query: isPrefixed - ? action.label.trimStart() - : queryContext.searchString, + // eslint-disable-next-line no-nested-ternary + query: isWorkspaceSearch + ? action.extraPayload.prettyName + : isPrefixed + ? action.label.trimStart() + : queryContext.searchString, icon: action.icon, shortcutContent: ownerGlobal.gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand( @@ -378,7 +393,9 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { !isPrefixed; let result = new lazy.UrlbarResult({ type: UrlbarUtils.RESULT_TYPE.DYNAMIC, - source: UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS, + source: isWorkspaceSearch + ? UrlbarUtils.RESULT_SOURCE.WORKSPACES + : UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS, payload, highlights: payloadHighlights, heuristic: shouldBePrioritized, @@ -398,7 +415,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider { zenUrlbarResultsLearner .sortCommandsByPriority(finalResults) .forEach(result => { - if (isPrefixed && i === 0 && query.length > 1) { + if (isPrefixed && !isWorkspaceSearch && i === 0 && query.length > 1) { result.heuristic = true; delete result.suggestedIndex; }