Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions background_scripts/all_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ const allCommands = [
name: "scrollToTop",
desc: "Scroll to the top of the page",
group: "navigation",
noRepeat: true,
},

{
name: "scrollToBottom",
desc: "Scroll to the bottom of the page",
group: "navigation",
noRepeat: true,
},

{
Expand Down Expand Up @@ -82,12 +84,15 @@ const allCommands = [
desc: "Scroll all the way to the left",
group: "navigation",
advanced: true,
noRepeat: true,
},

{
name: "scrollToRight",
desc: "Scroll all the way to the right",
group: "navigation",
advanced: true,
noRepeat: true,
},

{
Expand All @@ -98,6 +103,7 @@ const allCommands = [
options: {
hard: "Perform a hard reload, forcing the browser to bypass its cache.",
},
noRepeat: true,
},

{
Expand Down Expand Up @@ -137,6 +143,7 @@ const allCommands = [
desc: "Go to the root of current URL hierarchy",
group: "navigation",
advanced: true,
noRepeat: true,
},

{
Expand Down Expand Up @@ -175,6 +182,7 @@ const allCommands = [
name: "focusInput",
desc: "Focus the first text input on the page",
group: "navigation",
noRepeat: true,
},

{
Expand Down Expand Up @@ -303,6 +311,7 @@ const allCommands = [
},
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
Expand All @@ -315,6 +324,7 @@ const allCommands = [
"section of the Vimium Options page. The Vomnibar will be scoped to use that search engine.",
},
topFrame: true,
noRepeat: true,
},

{
Expand All @@ -325,6 +335,7 @@ const allCommands = [
query: "The text to prefill the Vomnibar with.",
},
topFrame: true,
noRepeat: true,
},

{
Expand All @@ -335,27 +346,39 @@ const allCommands = [
query: "The text to prefill the Vomnibar with.",
},
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateCommandSelection",
desc: "Execute a Vimium command",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateTabSelection",
desc: "Search through your open tabs",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateEditUrl",
desc: "Edit the current URL",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

{
name: "Vomnibar.activateEditUrlInNewTab",
desc: "Edit the current URL and open in a new tab",
group: "vomnibar",
topFrame: true,
noRepeat: true,
},

//
Expand Down Expand Up @@ -457,13 +480,15 @@ const allCommands = [
desc: "Go to the first tab",
group: "tabs",
background: true,
noRepeat: true,
},

{
name: "lastTab",
desc: "Go to the last tab",
group: "tabs",
background: true,
noRepeat: true,
},

{
Expand All @@ -479,6 +504,7 @@ const allCommands = [
desc: "Pin or unpin current tab",
group: "tabs",
background: true,
noRepeat: true,
},

{
Expand Down Expand Up @@ -517,6 +543,7 @@ const allCommands = [
group: "tabs",
advanced: true,
background: true,
noRepeat: true,
},

{
Expand Down Expand Up @@ -569,6 +596,7 @@ const allCommands = [
options: {
level: "The zoom level. This can be a range of [0.25, 5.0]. 1.0 is the default.",
},
noRepeat: true,
},

{
Expand All @@ -593,6 +621,7 @@ const allCommands = [
group: "tabs",
advanced: true,
background: true,
noRepeat: true,
},

//
Expand Down
27 changes: 14 additions & 13 deletions background_scripts/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,19 +369,19 @@ const Commands = {
// This is used by the help page and commands listing.
prepareHelpPageData() {
/*
Map of commands to option sets to keys to trigger that command option set.
Commands with no options will have the empty string options set.
Example:
{
"zoomReset": {
"": ["z0", "zz"] // No options, with two key maps, ie: `map zz zoomReset`
},
"setZoom": {
"1.1": ["z1"], // `map z1 setZoom 1.1`
"1.2": ["z2"], // `map z2 setZoom 1.2`
}
}
*/
Map of commands to option sets to keys to trigger that command option set.
Commands with no options will have the empty string options set.
Example:
{
"zoomReset": {
"": ["z0", "zz"] // No options, with two key maps, ie: `map zz zoomReset`
},
"setZoom": {
"1.1": ["z1"], // `map z1 setZoom 1.1`
"1.2": ["z2"], // `map z2 setZoom 1.2`
}
}
*/
const commandToOptionsToKeys = {};
const formatOptionString = (options) => {
return Object.entries(options)
Expand Down Expand Up @@ -455,6 +455,7 @@ const defaultKeyMappings = {
"T": "Vomnibar.activateTabSelection",
"b": "Vomnibar.activateBookmarks",
"B": "Vomnibar.activateBookmarksInNewTab",
":": "Vomnibar.activateCommandSelection",
"ge": "Vomnibar.activateEditUrl",
"gE": "Vomnibar.activateEditUrlInNewTab",

Expand Down
115 changes: 106 additions & 9 deletions background_scripts/completion/completers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, "&lt;").replace(/>/g, "&gt;");
};
const keybindings = this.command.keys.map((key) => `
<span class="key-block">
Copy link
Copy Markdown
Owner

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.

Copy link
Copy Markdown
Owner

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.

Copy link
Copy Markdown
Author

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.

<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 = `\
Expand Down Expand Up @@ -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) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done this way because the main loop uses commandsToOptionsToKeys to gather available commands. This means that non-mapped commands are not collected even though they are available. createUnboundRegistryEntry fills this gap.

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
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions background_scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as marks from "../background_scripts/marks.js";

import {
BookmarkCompleter,
CommandCompleter,
DomainCompleter,
HistoryCompleter,
MultiCompleter,
Expand Down Expand Up @@ -43,6 +44,7 @@ chrome.storage.session.set({ vimiumSecret: secretToken });

const completionSources = {
bookmarks: new BookmarkCompleter(),
commands: new CommandCompleter(),
history: new HistoryCompleter(),
domains: new DomainCompleter(),
tabs: new TabCompleter(),
Expand All @@ -58,6 +60,7 @@ const completers = {
completionSources.searchEngines,
]),
bookmarks: new MultiCompleter([completionSources.bookmarks]),
commands: new MultiCompleter([completionSources.commands]),
tabs: new MultiCompleter([completionSources.tabs]),
};

Expand Down Expand Up @@ -601,6 +604,12 @@ const sendRequestHandlers = {
runBackgroundCommand(request, sender) {
return BackgroundCommands[request.registryEntry.command](request, sender);
},
// "runNormalModeCommand" is used as a proxy in order to execute a command as if it was run in
// normal mode.
// The "request" must contain a "count" and a valid "command: RegistryEntry" parameter.
runNormalModeCommand(request, sender) {
chrome.tabs.sendMessage(sender.tab.id, request);
},
// getCurrentTabUrl is used by the content scripts to get their full URL, because window.location
// cannot help with Chrome-specific URLs like "view-source:http:..".
getCurrentTabUrl({ tab }) {
Expand Down
Loading