From f8da0deaac8c712e5434a5af44cfe2d3241b32c1 Mon Sep 17 00:00:00 2001 From: Mariusz Kaczmarczyk Date: Tue, 20 Oct 2020 00:02:33 +0200 Subject: [PATCH] Local and global marks --- readme.md | 6 ++ src/excmds.ts | 206 +++++++++++++++++++++++++++++++++++++++++++++- src/lib/config.ts | 2 + src/lib/webext.ts | 10 +++ src/state.ts | 19 ++++- 5 files changed, 241 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 09afaf6c..6b01261f 100644 --- a/readme.md +++ b/readme.md @@ -127,6 +127,12 @@ If you want to be able to use `` to search for things, use `` after op If you want to use Firefox's default `` binding to open the bookmarks sidebar, make sure to run `unbind ` because Tridactyl replaces this setting with one to go to the previous part of the page. +#### Marks + +- `m a-zA-Z` — set a local mark (lowercase letter), or a global mark (uppercase letter) +- `` ` a-zA-Z `` — jump to a local mark (lowercase letter), or a global mark (uppercase letter) +- ``` `` ``` — jump to the location before the last mark jump + #### Navigating to new pages: - `o`/`O` — open a URL (or default search) in this tab (`O` to pre-load current URL) diff --git a/src/excmds.ts b/src/excmds.ts index 36230883..b7ad64c6 100644 --- a/src/excmds.ts +++ b/src/excmds.ts @@ -74,9 +74,22 @@ // Shared import * as Messaging from "@src/lib/messaging" -import { ownWinTriIndex, getTriVersion, browserBg, activeTab, activeTabId, activeTabContainerId, openInNewTab, openInNewWindow, openInTab, queryAndURLwrangler } from "@src/lib/webext" +import { + ownWinTriIndex, + getTriVersion, + browserBg, + activeTab, + activeTabId, + activeTabContainerId, + openInNewTab, + openInNewWindow, + openInTab, + queryAndURLwrangler, + goToTab, +} from "@src/lib/webext" import * as Container from "@src/lib/containers" import state from "@src/state" +import * as State from "@src/state" import { contentState, ModeName } from "@src/content/state_content" import * as UrlUtil from "@src/lib/url_util" import * as config from "@src/lib/config" @@ -1043,6 +1056,185 @@ export function jumpprev(n = 1) { }) } +/** + * Jumps to a local mark, a global mark, or the location before the last mark jump. + * [a-z] are local marks, [A-Z] are global marks and '`' is the location before the last mark jump. + * @param key the key associated with the mark + */ +//#content +export async function markjump(key: string) { + if (key.length !== 1) { + throw new Error("markjump accepts only a single letter or '`'"); + } + if (key === "`") { + return markjumpbefore() + } + if (!/[a-z]/i.exec(key)) { + throw new Error("markjump accepts only a single letter or '`'"); + } + if (key === key.toUpperCase()) { + return markjumpglobal(key); + } + return markjumplocal(key); +} + +/** + * Jumps to a local mark. + * @param key the key associated with the mark + */ +//#content +export async function markjumplocal(key: string) { + const urlWithoutAnchor = window.location.href.split("#")[0] + const localMarks = await State.getAsync("localMarks"); + const mark = localMarks.get(urlWithoutAnchor)?.get(key); + if (mark) { + const currentTabId = await activeTabId(); + state.beforeJumpMark = { url: urlWithoutAnchor, scrollX: window.scrollX, scrollY: window.scrollY, tabId: currentTabId}; + scrolltab(currentTabId, mark.scrollX, mark.scrollY, `Jumped to mark '${key}'`); + } + return fillcmdline_tmp(3000, `Mark '${key}' is not set`); +} + +/** + * Jumps to a global mark. If the tab with the mark no longer exists or its url differs from the mark's url, + * jumps to another tab with the mark's url or creates it first if such tab does not exist. + * @param key the key associated with the mark + */ +//#content +export async function markjumpglobal(key: string) { + const globalMarks = await State.getAsync("globalMarks"); + const mark = globalMarks.get(key); + if (!mark) { + return fillcmdline_tmp(3000, `Mark '${key}' is not set`); + } + const currentTabId = await activeTabId(); + state.beforeJumpMark = { + url: window.location.href.split("#")[0], + scrollX: window.scrollX, + scrollY: window.scrollY, + tabId: currentTabId + }; + try { + const tab = await browserBg.tabs.get(mark.tabId); + return onTabExists(tab); + } catch (e) { + return onTabNoLongerValid(); + } + + async function onTabExists(tab) { + const tabUrl = tab.url.split("#")[0] + if (mark.url !== tabUrl) { + return onTabNoLongerValid(); + } + return goToTab(tab.id).then(() => { + scrolltab(tab.id, mark.scrollX, mark.scrollY, `Jumped to mark '${key}'`); + }); + } + + // the tab with mark's tabId doesn't exist or it has a different url than the mark's url + async function onTabNoLongerValid() { + const matchingTabs = await browserBg.tabs.query({url: mark.url}); + // If there are no matching tabs, open a new one and update the mark's tabId for future use in this session + if (!matchingTabs.length) { + return openInNewTab(mark.url).then(updateMarkAndScroll()); + } + // If there are multiple tabs open with the same url, just pick the first one and update the mark's tabId + // for future use in this session + return goToTab(matchingTabs[0].id).then(updateMarkAndScroll()); + } + + function updateMarkAndScroll() { + return (tab) => { + mark.tabId = tab.id; + state.globalMarks = globalMarks; + scrolltab(tab.id, mark.scrollX, mark.scrollY, `Jumped to mark '${key}'`); + }; + } +} + +/** + * Jumps to a location saved before the last mark jump as long as the tab it's located in exists and its url didn't change. + * Overwrites the location before the last mark jump - repeating this method will jump back and forth between two locations. + */ +//#content +export async function markjumpbefore() { + const beforeJumpMark = await State.getAsync("beforeJumpMark"); + if (!beforeJumpMark) { + return; + } + try { + const tab = await browserBg.tabs.get(beforeJumpMark.tabId); + const tabUrl = tab.url.split("#")[0] + const {url, scrollX, scrollY, tabId} = beforeJumpMark; + if (url !== tabUrl) { + return; + } + const currentTabId = await activeTabId(); + state.beforeJumpMark = {url: window.location.href.split("#")[0], scrollX: window.scrollX, scrollY: window.scrollY, tabId: currentTabId}; + goToTab(tabId).then(() => scrolltab(tabId, scrollX, scrollY, "Jumped to the last location before a mark jump")); + } catch (e) { + // the mark's tab is no longer valid + } +} + +/** + * Scrolls to a given position in a given tab and prints a message in it. + */ +//#content +export async function scrolltab(tabId: number, scrollX: number, scrollY: number, message: string) { + browserBg.tabs.executeScript({ + code: `window.scrollTo(${scrollX},${scrollY})` + }).then(() => acceptExCmd(tabId, [`fillcmdline_tmp 3000 ${message}`])); +} + +/** + * Adds a global or a local mark. Assigns a global mark to a key or a local mark the current url and a key. + * If a mark is already assigned, it is overwritten. + * @param key the key associated with the mark + */ +//#content +export async function markadd(key: string) { + if (!/[a-z]/i.exec(key) || key.length !== 1) { + throw new Error("markadd accepts only a single letter"); + } + if (key === key.toUpperCase()) { + return markaddglobal(key); + } + return markaddlocal(key); +} + +/** + * Assigns a local mark to the current url and the given key. If a mark is already assigned, it is overwritten. + * Two urls are considered the same if they're identical ignoring anchors. + * Local marks are not persisted between browser restarts. + */ +//#content +export async function markaddlocal(key: string) { + const urlWithoutAnchor = window.location.href.split("#")[0] + const localMarks = await State.getAsync("localMarks"); + const localUrlMarks = localMarks.get(urlWithoutAnchor) ? localMarks.get(urlWithoutAnchor) : new Map(); + const newLocalMark = { scrollX: window.scrollX, scrollY: window.scrollY}; + localUrlMarks.set(key, newLocalMark); + localMarks.set(urlWithoutAnchor, localUrlMarks); + state.localMarks = localMarks; + fillcmdline_tmp(3000, `Mark '${key}' set`); +} + +/** + * Assigns a global mark to the given key. If a mark is already assigned, it is overwritten. + * Global marks are persisted between browser restarts. + */ +//#content +export async function markaddglobal(key: string) { + const urlWithoutAnchor = window.location.href.split("#")[0] + const globalMarks = await State.getAsync("globalMarks"); + const tabId = await activeTabId() + const newGlobalMark = { url: urlWithoutAnchor, scrollX: window.scrollX, scrollY: window.scrollY, tabId }; + globalMarks.set(key, newGlobalMark); + state.globalMarks = globalMarks; + fillcmdline_tmp(3000, `Mark '${key}' set`); +} + /** Called on 'scroll' events. If you want to have a function that moves within the page but doesn't add a location to the jumplist, make sure to set JUMPED to true before moving @@ -4734,6 +4926,18 @@ export function jumble() { body.currentNode.textContent = jumble_helper(t) } } + +/** + * Run a command in a tab with an internal firefox identifier specified by destination or in background if destination is "background". + */ +//#content +export function acceptExCmd(destination: number | "background", ex_string: string[]) { + if (destination === "background") { + return run_exstr(...ex_string); + } + return Messaging.messageTab(destination, "controller_content", "acceptExCmd", ex_string) +} + /** * Hacky ex string parser. * diff --git a/src/lib/config.ts b/src/lib/config.ts index 8eb231a8..d3e6ea0b 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -370,6 +370,8 @@ export class default_config { ".": "repeat", "ba": "open https://www.youtube.com/watch?v=M3iOROuTuMA", + "m": "gobble 1 markadd", + "`": "gobble 1 markjump", } vmaps = { diff --git a/src/lib/webext.ts b/src/lib/webext.ts index ad408a9f..5c236240 100644 --- a/src/lib/webext.ts +++ b/src/lib/webext.ts @@ -327,3 +327,13 @@ export async function openInTab(tab, opts = {}, strarr: string[]) { Object.assign({ url: "/static/newtab.html" }, opts), ) } + +/** + * Set active the tab with tabId and focus the window it is located in. + * @param tabId tab identifier + */ +export async function goToTab(tabId: number) { + const tab = await browserBg.tabs.update(tabId, { active: true }); + await browserBg.windows.update(tab.windowId, { focused: true }); + return tab; +} diff --git a/src/state.ts b/src/state.ts index 2ddedd08..c8d236fe 100644 --- a/src/state.ts +++ b/src/state.ts @@ -28,10 +28,27 @@ class State { }, ] last_ex_str = "echo" + globalMarks: Map = new Map() + localMarks: Map> = new Map() + beforeJumpMark: { + url: string, + scrollX: number, + scrollY: number, + tabId: number + } = undefined } // Store these keys in the local browser storage to persist between restarts -const PERSISTENT_KEYS: Array = ["cmdHistory"] +const PERSISTENT_KEYS: Array = ["cmdHistory", "globalMarks"] // Don't change these from const or you risk breaking the Proxy below. const defaults = Object.freeze(new State())