-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[feature] Command mode #4880
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: master
Are you sure you want to change the base?
[feature] Command mode #4880
Changes from all commits
adfb5b8
713a239
19d6cc5
b08a12b
1ed1fea
29d205a
1664c8d
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 |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ import * as bgUtils from "./../bg_utils.js"; | |
| import * as completionSearch from "./search_wrapper.js"; | ||
| import * as userSearchEngines from "../user_search_engines.js"; | ||
| import * as ranking from "./ranking.js"; | ||
| import { allCommands } from "../all_commands.js"; | ||
| import { Commands, RegistryEntry } from "../commands.js"; | ||
| import { RegexpCache } from "./ranking.js"; | ||
|
|
||
| // Set this to true to render relevancy when debugging the ranking scores. | ||
|
|
@@ -47,6 +49,14 @@ export class Suggestion { | |
| tabId; | ||
| // Whether this is a suggestion provided by a user's custom search engine. | ||
| isCustomSearch; | ||
| // Suggestion in "command" mode. | ||
| // command = { | ||
| // "RegistryEntry" to execute the command in "NormalMode.commandHandler". | ||
| // registryEntry: RegistryEntry, | ||
| // Key mapping to show in the omni bar suggestions. | ||
| // keys: Array[string] | ||
| // } | ||
| command; | ||
| // Whether this is meant to be the first suggestion from the user's custom search engine which | ||
| // represents their query as typed, verbatim. | ||
| isPrimarySuggestion = false; | ||
|
|
@@ -93,6 +103,23 @@ export class Suggestion { | |
| <span class="title">${this.highlightQueryTerms(Utils.escapeHtml(this.title))}</span> | ||
| ${relevancyHtml} | ||
| </div>\ | ||
| `; | ||
| } else if (this.command) { | ||
| // Key mappings containing key modifiers are represented in the form of '<modifier-key>' | ||
| // (e.g <c-e>) and are parsed as HTML tags when used in a raw string. Escape them properly. | ||
| const escapeKeyForHtml = (key) => { | ||
| return key.replace(/</g, "<").replace(/>/g, ">"); | ||
| }; | ||
| const keybindings = this.command.keys.map((key) => ` | ||
| <span class="key-block"> | ||
| <span class="key">${escapeKeyForHtml(key)}</span> | ||
| <span class="comma">, </span> | ||
| </span>`).join("\n"); | ||
| this.html = ` | ||
| <div class="top-half"> | ||
| <span class="source ${insertTextClass}">${insertTextIndicator}</span><span class="source">${this.description}</span> | ||
| <span class="title">${this.highlightQueryTerms(this.title)}</span>${keybindings}${relevancyHtml} | ||
| </div> | ||
| `; | ||
| } else { | ||
| this.html = `\ | ||
|
|
@@ -353,6 +380,81 @@ export class BookmarkCompleter { | |
| } | ||
| } | ||
|
|
||
| export class CommandCompleter { | ||
| async filter({ queryTerms }) { | ||
| // Get the key mapping for a command. | ||
| // Each entry contains the user-specified options and an array of possible mappings. | ||
| // Example: | ||
| // "closeTabsOnRight" : { | ||
| // "count=2": ["c2l", "c2k"], | ||
| // "count=3": ["c3l", "c3k"], | ||
| // } | ||
| const commandToOptionsToKeys = | ||
| (await chrome.storage.session.get("commandToOptionsToKeys")).commandToOptionsToKeys; | ||
|
|
||
| // Create a RegistryEntry for the default action (no options specified) of a command. | ||
| const createUnboundRegistryEntry = (command) => { | ||
|
Owner
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. I haven't looked closely at why this is needed, but it would be nice if we didn't have to have this.
Author
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 is done this way because the main loop uses To be honest this part of the implementation seemed quite obscure to me and it also made for some weird test programming that you have already commented on in the other threads. I am open to suggestions if I am missing an obvious and easier way to do this. |
||
| return new RegistryEntry({ | ||
| keySequence: [], | ||
| command: command.name, | ||
| noRepeat: command.noRepeat, | ||
| repeatLimit: command.repeatLimit, | ||
| background: command.background, | ||
| topFrame: command.topFrame, | ||
| options: {}, | ||
| }); | ||
| }; | ||
|
|
||
| const matchingCommands = allCommands.filter((command) => | ||
| ranking.matches(queryTerms, command.desc) | ||
| ); | ||
|
|
||
| let suggestions = []; | ||
| for (const commandInfo of matchingCommands) { | ||
| const variations = commandToOptionsToKeys[commandInfo.name] || {}; | ||
|
|
||
| // Indicates if the default action of the command (no additional options) is bound to a key. | ||
| const isDefaultBound = Object.keys(variations).some((option) => option.length === 0); | ||
|
|
||
| // If the default action is not bound, add the entry explicitly to the suggestions. | ||
| // This makes unbound commands accessible from the omni bar in 'command' mode. | ||
| if (!isDefaultBound) { | ||
| suggestions.push( | ||
| new Suggestion({ | ||
| queryTerms, | ||
| description: "command", | ||
| title: commandInfo.desc, | ||
| deDuplicate: false, | ||
| command: { | ||
| registryEntry: createUnboundRegistryEntry(commandInfo), | ||
| keys: [], | ||
| }, | ||
| relevancy: 1, | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| // Add all bound/mapped command variations to the suggestions. | ||
| for (const [options, keys] of Object.entries(variations)) { | ||
| suggestions.push( | ||
| new Suggestion({ | ||
| queryTerms, | ||
| description: "command", | ||
| title: commandInfo.desc + (options ? ` (${options})` : ""), | ||
| deDuplicate: false, | ||
| command: { | ||
| registryEntry: Commands.keyToRegistryEntry[keys[0]], | ||
| keys: keys, | ||
| }, | ||
| relevancy: 1, | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
| return suggestions; | ||
| } | ||
| } | ||
|
|
||
| export class HistoryCompleter { | ||
| // - seenTabToOpenCompletionList: true if the user has typed only <Tab>, and nothing else. | ||
| // We interpret this to mean that they want to see all of their history in the Vomnibar, sorted | ||
|
|
@@ -673,17 +775,12 @@ export class MultiCompleter { | |
| } | ||
| suggestions.sort((a, b) => b.relevancy - a.relevancy); | ||
|
|
||
| // Simplify URLs and remove duplicates (duplicate simplified URLs, that is). | ||
| let count = 0; | ||
| const seenUrls = {}; | ||
|
|
||
| const dedupedSuggestions = []; | ||
| for (const s of suggestions) { | ||
| const url = s.shortenUrl(); | ||
| if (s.deDuplicate && seenUrls[url]) continue; | ||
| if (count++ === maxResults) break; | ||
| seenUrls[url] = s; | ||
| dedupedSuggestions.push(s); | ||
| if (dedupedSuggestions.length === maxResults) break; | ||
| if (!s.deDuplicate || !dedupedSuggestions.includes(s.shortenUrl())) { | ||
| dedupedSuggestions.push(s); | ||
| } | ||
| } | ||
|
|
||
| // Give each completer the opportunity to tweak the suggestions. | ||
|
|
||
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.
Note that this section is related to help_dialog_page.js, which also generates keys using similar CSS class names, even though no code is shared.
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.
I don't have a strong opinion on whether it's warranted here, but the more formatting work we do in Javascript, the more we would benefit from using an HTML template and populating it using DOM APIs, as the help dialog does.
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.
I agree. I would prefer to leave this for some other PR though. You probably have a better idea of how to refactor this properly.