Skip to content
Draft
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
36 changes: 36 additions & 0 deletions background_scripts/all_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,42 @@ const allCommands = [
advanced: true,
},

{
name: "LinkHints.activateModeToCopyCodeBlock",
desc: "Copy a code block's text to the clipboard",
group: "navigation",
advanced: true,
noRepeat: true,
},
{
name: "LinkHints.activateModeToCopyParagraph",
desc: "Copy a paragraph's text to the clipboard",
group: "navigation",
advanced: true,
noRepeat: true,
},
{
name: "LinkHints.activateModeToCopySpan",
desc: "Copy a span's text to the clipboard",
group: "navigation",
advanced: true,
noRepeat: true,
},
{
name: "LinkHints.activateModeToCopyAnchor",
desc: "Copy an anchor's text to the clipboard",
group: "navigation",
advanced: true,
noRepeat: true,
},
{
name: "LinkHints.activateModeToCopyAny",
desc: "Copy text from various element types (a, p, span, code, pre)",
group: "navigation",
advanced: true,
noRepeat: true,
},

{
name: "goPrevious",
desc: "Follow the link labeled previous or <",
Expand Down
7 changes: 7 additions & 0 deletions background_scripts/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,13 @@ const defaultKeyMappings = {
"v": "enterVisualMode",
"V": "enterVisualLineMode",

// Copy element text (prefix c): cc (code), cp (paragraph), cs (span), ca (anchor), c* (any)
"cc": "LinkHints.activateModeToCopyCodeBlock",
"cp": "LinkHints.activateModeToCopyParagraph",
"cs": "LinkHints.activateModeToCopySpan",
"ca": "LinkHints.activateModeToCopyAnchor",
"c*": "LinkHints.activateModeToCopyAny",

// Link hints
"f": "LinkHints.activateMode",
"F": "LinkHints.activateModeToOpenInNewTab",
Expand Down
156 changes: 154 additions & 2 deletions content_scripts/link_hints.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,70 @@ const FOCUS_LINK = {
},
};

// Generic copy modes (selector-based). Each provides customSelectors array for hint gathering.
function copyElementTextActivator(label) {
return function (el) {
if (!el) return;
let text = el.innerText || el.textContent || "";
if (!text.trim()) {
HUD.show(`No ${label} text to yank.`, 1500);
return;
}
HUD.copyToClipboard(text);
const preview = text.replace(/\s+/g, " ").trim().slice(0, 40);
HUD.show(`Yanked ${label}: ${preview}${text.length > 40 ? "..." : ""}`, 2000);
};
}

const COPY_CODE_BLOCK = {
name: "code",
indicator: "Copy code block",
customSelectors: [
"pre",
"code",
".hljs",
"div.highlight pre",
"div.highlight code",
"[data-code-block]",
],
linkActivator: copyElementTextActivator("code"),
};
const COPY_PARAGRAPH_BLOCK = {
name: "para",
indicator: "Copy paragraph",
customSelectors: ["p"],
linkActivator: copyElementTextActivator("paragraph"),
};
const COPY_SPAN_BLOCK = {
name: "span",
indicator: "Copy span",
customSelectors: ["span"],
linkActivator: copyElementTextActivator("span"),
};
const COPY_ANCHOR_BLOCK = {
name: "alink",
indicator: "Copy link text",
customSelectors: ["a"],
linkActivator: copyElementTextActivator("link"),
};
const COPY_ANY_BLOCK = {
name: "any",
indicator: "Copy element",
// Union of existing selector groups (anchor, span, paragraph, code family).
customSelectors: [
"a",
"span",
"p",
"pre",
"code",
".hljs",
"div.highlight pre",
"div.highlight code",
"[data-code-block]",
],
linkActivator: copyElementTextActivator("element"),
};

const availableModes = [
OPEN_IN_CURRENT_TAB,
OPEN_IN_NEW_BG_TAB,
Expand All @@ -157,6 +221,11 @@ const availableModes = [
COPY_LINK_TEXT,
HOVER_LINK,
FOCUS_LINK,
COPY_CODE_BLOCK,
COPY_PARAGRAPH_BLOCK,
COPY_SPAN_BLOCK,
COPY_ANCHOR_BLOCK,
COPY_ANY_BLOCK,
];

const HintCoordinator = {
Expand Down Expand Up @@ -222,14 +291,19 @@ const HintCoordinator = {
getHintDescriptors({ modeIndex, requestedByHelpDialog }, _sender) {
if (!DomUtils.isReady() || DomUtils.windowIsTooSmall()) return [];

const requireHref = [COPY_LINK_URL, OPEN_INCOGNITO].includes(availableModes[modeIndex]);
const mode = availableModes[modeIndex];
const requireHref = [COPY_LINK_URL, OPEN_INCOGNITO].includes(mode);
// If link hints is launched within the help dialog, then we only offer hints from that frame.
// This improves the usability of the help dialog on the options page (particularly for
// selecting command names).
if (requestedByHelpDialog && !globalThis.isVimiumHelpDialog) {
this.localHints = [];
} else {
this.localHints = LocalHints.getLocalHints(requireHref);
if (mode.customSelectors) {
this.localHints = LocalHints.getHintsForSelectors(mode.customSelectors);
} else {
this.localHints = LocalHints.getLocalHints(requireHref);
}
}
this.localHintDescriptors = this.localHints.map(({ linkText }, localIndex) => (
new HintDescriptor({
Expand Down Expand Up @@ -484,6 +558,52 @@ class LinkHintsMode {
// Note that Vimium's CSS is user-customizable. We're adding the "vimiumHintMarker" class here
// for users to customize. See further comments about this in vimium.css.
el.className = "vimium-reset internal-vimium-hint-marker vimiumHintMarker";

// Classify the element so we can style different hint types differently (buttons, links,
// external links). We only attach these classes for local markers actually rendered here.
try {
const target = localHint.element;
if (target) {
const tag = target.tagName?.toLowerCase?.() || "";
const role = target.getAttribute?.("role")?.toLowerCase?.();
let typeClass = null;

// Helper to decide if an <a> is external (different domain).
const classifyAnchor = () => {
if (!target.href) return "vimium-hint-type-link"; // No href => treat as normal link.
let linkHostname;
try {
linkHostname = new URL(target.href, document.baseURI).hostname || "";
} catch (_) {
linkHostname = ""; // Malformed; treat as same-domain link.
}
const norm = (h) => h.replace(/^www\./, "");
if (linkHostname && norm(linkHostname) && norm(linkHostname) !== norm(location.hostname || "")) {
return "vimium-hint-type-external";
}
return "vimium-hint-type-link";
};

if (
tag === "button" ||
(tag === "input" && ["button", "submit", "reset"].includes((target.getAttribute("type") || "").toLowerCase())) ||
role === "button"
) {
typeClass = "vimium-hint-type-button";
} else if (tag === "a" || role === "link") {
typeClass = classifyAnchor();
} else if (target.href && tag !== "area") { // Generic clickable with href (e.g. <div role=link>
typeClass = classifyAnchor();
}

// Apply class if determined.
if (typeClass) {
el.classList.add(typeClass);
}
}
} catch (_) {
// Swallow any classification errors; hint rendering must not break.
}
Object.assign(marker, {
element: el,
localHint,
Expand Down Expand Up @@ -1548,4 +1668,36 @@ Object.assign(globalThis, {
AlphabetHints,
FilterHints,
WaitForEnter,
COPY_CODE_BLOCK,
COPY_PARAGRAPH_BLOCK,
COPY_SPAN_BLOCK,
COPY_ANCHOR_BLOCK,
COPY_ANY_BLOCK,
});
// Generic selector-based helper for custom copy modes.
LocalHints.getHintsForSelectors = function (selectors) {
if (!document.documentElement) return [];
let elements = [];
try {
elements = selectors.flatMap((sel) => Array.from(document.querySelectorAll(sel)));
} catch (_) {
return [];
}
elements = elements.filter((el) => !elements.some((other) => (other !== el) && other.contains(el)));
const hints = [];
const { top: viewportTop, left: viewportLeft } = DomUtils.getViewportTopLeft();
for (const el of elements) {
const rect = DomUtils.getVisibleClientRect(el, true);
if (!rect) continue;
if (rect.width < 10 || rect.height < 10) continue;
let text = (el.innerText || el.textContent || "").trim();
if (!text) continue;
let caption = text.split(/\n/)[0];
if (caption.length > 80) caption = caption.slice(0, 77) + "...";
// Adjust to document coordinates (LinkHints expects rect relative to full document, not viewport).
rect.top += viewportTop;
rect.left += viewportLeft;
hints.push(new LocalHint({ element: el, rect, linkText: caption, showLinkText: true }));
}
return hints;
};
6 changes: 6 additions & 0 deletions content_scripts/mode_normal.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ const NormalModeCommands = {
"LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind(LinkHints),
"LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind(LinkHints),
"LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind(LinkHints),
// Custom: copy element text via hinting (prefix 'c').
"LinkHints.activateModeToCopyCodeBlock": function () { HintCoordinator.prepareToActivateMode(COPY_CODE_BLOCK, function () {}); },
"LinkHints.activateModeToCopyParagraph": function () { HintCoordinator.prepareToActivateMode(COPY_PARAGRAPH_BLOCK, function () {}); },
"LinkHints.activateModeToCopySpan": function () { HintCoordinator.prepareToActivateMode(COPY_SPAN_BLOCK, function () {}); },
"LinkHints.activateModeToCopyAnchor": function () { HintCoordinator.prepareToActivateMode(COPY_ANCHOR_BLOCK, function () {}); },
"LinkHints.activateModeToCopyAny": function () { HintCoordinator.prepareToActivateMode(COPY_ANY_BLOCK, function () {}); },

"Vomnibar.activate": Vomnibar.activate.bind(Vomnibar),
"Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind(Vomnibar),
Expand Down
22 changes: 22 additions & 0 deletions content_scripts/vimium.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ div.internal-vimium-hint-marker {
z-index: 2147483647;
}

/* Hint type variants: these classes are added dynamically in link_hints.js.
* We keep them additive so user-defined CSS can override them easily. */
.internal-vimium-hint-marker.vimium-hint-type-button {
background: linear-gradient(to bottom, #d0ebff 0%, #74c0fc 100%);
border-color: #339af0;
}
.internal-vimium-hint-marker.vimium-hint-type-link {
background: linear-gradient(to bottom, #e6ffe6 0%, #9bde83 100%);
border-color: #4caf50;
}
.internal-vimium-hint-marker.vimium-hint-type-external {
background: linear-gradient(to bottom, #ffe6f1 0%, #ff87b5 100%);
border-color: #ff3d7f;
}

/* Active state tweaks keep foreground legible */
.internal-vimium-hint-marker.vimium-hint-type-external span,
.internal-vimium-hint-marker.vimium-hint-type-button span,
.internal-vimium-hint-marker.vimium-hint-type-link span {
color: #302505;
}

div.internal-vimium-hint-marker span {
color: #302505;
font-family: Helvetica, Arial, sans-serif;
Expand Down