Merge branch 'make_find_faster'

This commit is contained in:
Oliver Blanthorn 2019-01-22 15:44:48 +00:00
commit ba0dea0349
No known key found for this signature in database
GPG key ID: 2BB8C36BB504BFF3
15 changed files with 493 additions and 163 deletions

28
package-lock.json generated
View file

@ -4915,12 +4915,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4935,17 +4937,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -5062,7 +5067,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -5074,6 +5080,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5088,6 +5095,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5095,12 +5103,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -5119,6 +5129,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -5199,7 +5210,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -5211,6 +5223,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5332,6 +5345,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

View file

@ -21,6 +21,7 @@ import * as perf from "@src/perf"
import "@src/lib/number.clamp"
import "@src/lib/html-tagged-template"
import * as Completions from "@src/completions"
import { FindCompletionSource } from "./completions/Find"
import { TabAllCompletionSource } from "@src/completions/TabAll"
import { BufferCompletionSource } from "@src/completions/Tab"
import { BmarkCompletionSource } from "@src/completions/Bmark"
@ -94,6 +95,7 @@ function getCompletion() {
export function enableCompletions() {
if (!activeCompletions) {
activeCompletions = [
// FindCompletionSource,
BmarkCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,

118
src/completions/Find.ts Normal file
View file

@ -0,0 +1,118 @@
import { browserBg, activeTabId } from "@src/lib/webext"
import * as Messaging from "@src/lib/messaging"
import * as Completions from "../completions"
import { executeWithoutCommandLine } from "@src/content/commandline_content"
import * as config from "@src/lib/config"
class FindCompletionOption extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
constructor(m, reverse = false) {
super()
this.value =
(reverse ? "-? " : "") + ("-: " + m.index) + " " + m.rangeData.text
this.fuseKeys.push(m.rangeData.text)
let contextLength = 4
// Create HTMLElement
this.html = html`<tr class="FindCompletionOption option">
<td class="content">${m.precontext}<span class="match">${
m.rangeData.text
}</span>${m.postcontext}</td>
</tr>`
}
}
export class FindCompletionSource extends Completions.CompletionSourceFuse {
public options: FindCompletionOption[]
public prevCompletion = null
public completionCount = 0
private startingPosition = 0
constructor(private _parent) {
super(["find "], "FindCompletionSource", "Matches")
this.startingPosition = window.pageYOffset
this._parent.appendChild(this.node)
}
async onInput(exstr) {
let id = this.completionCount++
// If there's already a promise being executed, wait for it to finish
await this.prevCompletion
// Since we might have awaited for this.prevCompletion, we don't have a guarantee we're the last completion the user asked for anymore
if (id == this.completionCount - 1) {
// If we are the last completion
this.prevCompletion = this.updateOptions(exstr)
await this.prevCompletion
}
}
private async updateOptions(exstr?: string) {
if (!exstr) return
// Flag parsing because -? should reverse completions
let tokens = exstr.split(" ")
let flagpos = tokens.indexOf("-?")
let reverse = flagpos >= 0
if (reverse) {
tokens.splice(flagpos, 1)
}
let query = tokens.slice(1).join(" ")
let minincsearchlen = await config.getAsync("minincsearchlen")
// No point if continuing if the user hasn't started searching yet
if (query.length < minincsearchlen) return
let findresults = await config.getAsync("findresults")
let incsearch = (await config.getAsync("incsearch")) === "true"
if (findresults === 0 && !incsearch) return
let incsearchonly = false
if (findresults === 0) {
findresults = 1
incsearchonly = true
}
// Note: the use of activeTabId here might break completions if the user starts searching for a pattern in a really big page and then switches to another tab.
// Getting the tabId should probably be done in the constructor but you can't have async constructors.
let tabId = await activeTabId()
let findings = await Messaging.messageTab(
tabId,
"finding_content",
"find",
[query, findresults, reverse],
)
// If the search was successful
if (findings.length > 0) {
// Get match context
let len = await config.getAsync("findcontextlen")
let matches = await Messaging.messageTab(
tabId,
"finding_content",
"getMatches",
[findings, len],
)
if (incsearch)
Messaging.messageTab(tabId, "finding_content", "jumpToMatch", [
query,
false,
0,
])
if (!incsearchonly) {
this.options = matches.map(
m => new FindCompletionOption(m, reverse),
)
this.updateChain(exstr, this.options)
}
}
}
// Overriding this function is important, the default one has a tendency to hide options when you don't expect it
setStateFromScore(scoredOpts, autoselect) {
this.options.forEach(o => (o.state = "normal"))
}
}

View file

@ -66,8 +66,8 @@ import * as styling from "@src/content/styling"
config,
dom,
excmds,
hinting_content,
finding_content,
hinting_content,
itertools,
logger,
Mark,

View file

@ -6,7 +6,6 @@ import * as messaging from "@src/lib/messaging"
import { parser as exmode_parser } from "@src/parsers/exmode"
import * as hinting from "@src/content/hinting"
import * as finding from "@src/content/finding"
import * as gobblemode from "@src/parsers/gobblemode"
import * as generic from "@src/parsers/genericmode"
@ -47,7 +46,6 @@ function* ParserController() {
input: keys => generic.parser("inputmaps", keys),
ignore: keys => generic.parser("ignoremaps", keys),
hint: hinting.parser,
find: finding.parser,
gobble: gobblemode.parser,
}
@ -69,8 +67,7 @@ function* ParserController() {
if (
currentMode !== "ignore" &&
currentMode !== "hint" &&
currentMode !== "input" &&
currentMode !== "find"
currentMode !== "input"
) {
if (textEditable) {
if (currentMode !== "insert") {

View file

@ -1,157 +1,266 @@
/** Find mode.
TODO:
important
n/N
?
show command line with user input?
performance
allow spaces
*/
import * as DOM from "@src/lib/dom"
import { hasModifiers } from "@src/lib/keyseq"
import { contentState } from "@src/content/state_content"
import { messageActiveTab, message } from "@src/lib/messaging"
// This file has various utilities used by the completion source for the find excmd
import * as Messaging from "@src/lib/messaging"
import * as config from "@src/lib/config"
import Logger from "@src/lib/logging"
import Mark from "mark.js"
const logger = new Logger("finding")
import * as DOM from "@src/lib/dom"
import { zip } from "@src/lib/itertools"
import { browserBg, activeTabId } from "@src/lib/webext"
function elementswithtext() {
return DOM.getElemsBySelector(
"*",
// offsetHeight tells us which elements are drawn
[
hint => {
return (
(hint as any).offsetHeight > 0 &&
(hint as any).offsetHeight !== undefined
)
},
hint => {
return hint.textContent != ""
},
],
export class Match {
constructor(
public index,
public rangeData,
public rectData,
public precontext,
public postcontext,
public firstNode,
) {}
}
function isCommandLineNode(n) {
let url = n.ownerDocument.location.href
return (
url.protocol == "moz-extension:" &&
url.pathname == "/static/commandline.html"
)
}
/** Simple container for the state of a single frame's finds. */
class findState {
readonly findHost = document.createElement("div")
public mark = new Mark(elementswithtext())
// ^ why does filtering by offsetHeight NOT work here
public markedels = []
public markpos = 0
public direction: 1 | -1 = 1
constructor() {
this.findHost.classList.add("TridactylfindHost")
}
public filter = ""
/** Get all text nodes within the page.
TODO: cache the results. I tried to do it but since we need to invalidate the cache when nodes are added/removed from the page, the results are constantly being invalidated by the completion buffer.
The solution is obviously to pass `document.body` to createTreeWalker instead of just `document` but then you won't get all the text nodes in the page and this is a big problem because the results returned by browser.find.find() need absolutely all nodes existing within the page, even the ones belonging the commandline. */
function getNodes() {
let nodes = []
let walker = document.createTreeWalker(
document,
NodeFilter.SHOW_TEXT,
null,
false,
)
let node = walker.nextNode()
do {
nodes.push(node)
node = walker.nextNode()
} while (node)
destructor() {
// Remove all finds from the DOM.
this.findHost.remove()
}
return nodes
}
let findModeState: findState = undefined
let lastMatches = []
/** Given "findings", an array matching the one returned by find(), will compute the context for every match in findings and prune the matches than happened in Tridactyl's command line. ContextLength is how many characters from before and after the match should be included into the returned Match object.
getMatches() will save its returned values in lastMatches. This is important for caching purposes in jumpToMatch, read its documentation to get the whole picture. */
export function getMatches(findings, contextLength = 10): Match[] {
let result = []
/** For each findable element, add a find */
if (findings.length == 0) return result
/** Show only finds prefixed by fstr. Focus first match */
function filter(fstr) {
// for some reason, doing the mark in the done function speeds this up immensely
// nb: https://jsfiddle.net/julmot/973gdh8g/ is pretty much what we want
findModeState.mark.unmark({
done: () => {
findModeState.mark.mark(fstr, {
separateWordSearch: false,
acrossElements: true,
})
},
})
}
// Checks if a node belongs to the command line
let nodes = getNodes()
/** Remove all finds, reset STATE. */
function reset(args?) {
if (args.leavemarks == "false") findModeState.mark.unmark()
if (args.unfind == "true") {
findModeState.mark.unmark()
findModeState.destructor()
findModeState = undefined
}
contentState.mode = "normal"
}
for (let i = 0; i < findings.length; ++i) {
let range = findings[i][0]
let firstnode = nodes[range.startTextNodePos]
let lastnode = nodes[range.endTextNodePos]
// We never want to match against nodes in the command line
if (
!firstnode ||
!lastnode ||
isCommandLineNode(firstnode) ||
isCommandLineNode(lastnode)
)
continue
function mode(mode: "nav" | "search") {
if (mode == "nav") {
// really, this should happen all the time when in search - we always want first result to be green and the window to move to it (if not already on screen)
findModeState.markedels = Array.from(
window.document.getElementsByTagName("mark"),
).filter(el => el.offsetHeight > 0)
// ^ why does filtering by offsetHeight work here
findModeState.markpos = 0
let el = findModeState.markedels[0]
if (el) {
if (!DOM.isVisible(el)) el.scrollIntoView()
// colour of the selected link
el.style.background = "lawngreen"
} else {
messageActiveTab("commandline_frame", "fillcmdline", [
"# Couldn't find pattern: " + findModeState.filter,
])
// Get the context before the match
let precontext = firstnode.textContent.substring(
range.startOffset - contextLength,
range.startOffset,
)
if (precontext.length < contextLength) {
let missingChars = contextLength - precontext.length
let id = range.startTextNodePos - 1
while (missingChars > 0 && nodes[id]) {
let txt = nodes[id].textContent
precontext =
txt.substring(txt.length - missingChars, txt.length) +
precontext
missingChars = contextLength - precontext.length
id -= 1
}
}
// Get the context after the match
let postcontext = lastnode.textContent.substr(
range.endOffset,
contextLength,
)
// If the last node doesn't have enough context and if there's a node after it
if (
postcontext.length < contextLength &&
nodes[range.endTextNodePos + 1]
) {
// Add text from the following text node to the context
postcontext += nodes[range.endTextNodePos + 1].textContent.substr(
0,
contextLength - postcontext.length,
)
}
result.push(
new Match(
i,
findings[i][0],
findings[i][1],
precontext,
postcontext,
firstnode,
),
)
}
lastMatches = result
return result
}
import "@src/lib/number.mod"
export function navigate(n: number = 1) {
// also - really - should probably actually make this be an excmd
// people will want to be able to scroll and stuff.
// should probably move this to an update function?
// don't hardcode this colour
findModeState.markedels[findModeState.markpos].style.background = "yellow"
findModeState.markpos = (
findModeState.markpos +
n * findModeState.direction
).mod(findModeState.markedels.length)
// obvs need to do mod to wrap indices
let el = findModeState.markedels[findModeState.markpos]
if (!DOM.isVisible(el)) el.scrollIntoView()
el.style.background = "lawngreen"
}
let prevFind = null
let findCount = 0
/** Performs a call to browser.find.find() with the right parameters and returns the result as a zipped array of rangeData and rectData (see browser.find.find's documentation) sorted according to their vertical position within the document.
If count is different from -1 and lower than the number of matches returned by browser.find.find(), will return count results. Note that when this happens, `matchesCacheIsValid ` is set to false, which will prevent `jumpToMatch` from using cached matches. */
export async function find(query, count = -1, reverse = false) {
findCount += 1
let findId = findCount
let findcase = await config.getAsync("findcase")
let caseSensitive =
findcase == "sensitive" ||
(findcase == "smart" && query.search(/[A-Z]/) >= 0)
let tabId = await activeTabId()
export function findPage(direction?: 1 | -1) {
if (findModeState !== undefined) reset({ unfind: "true" })
contentState.mode = "find"
findModeState = new findState()
if (direction !== undefined) findModeState.direction = direction
document.body.appendChild(findModeState.findHost)
}
// No point in searching for something that won't be used anyway
await prevFind
if (findId != findCount) return []
/** If key is in findchars, add it to filtstr and filter */
function pushKey(ke) {
if (ke.key === "Backspace") {
findModeState.filter = findModeState.filter.slice(0, -1)
filter(findModeState.filter)
} else if (ke.key === "Enter") {
mode("nav")
reset({ leavemarks: "true" })
} else if (ke.key === "Escape") {
reset({ unfind: "true" })
} else if (ke.key.length > 1) {
return
} else {
findModeState.filter += ke.key
filter(findModeState.filter)
prevFind = browserBg.find.find(query, {
tabId,
caseSensitive,
includeRangeData: true,
includeRectData: true,
})
let findings = await prevFind
findings = zip(findings.rangeData, findings.rectData).sort(
(a: any, b: any) => {
a = a[1].rectsAndTexts.rectList[0]
b = b[1].rectsAndTexts.rectList[0]
if (!a || !b) return 0
return a.top - b.top
},
)
let finder = e =>
e[1].rectsAndTexts.rectList[0] &&
e[1].rectsAndTexts.rectList[0].top > window.pageYOffset
if (reverse) {
findings = findings.reverse()
finder = e =>
e[1].rectsAndTexts.rectList[0] &&
e[1].rectsAndTexts.rectList[0].top < window.pageYOffset
}
let pivot = findings.indexOf(findings.find(finder))
findings = findings.slice(pivot).concat(findings.slice(0, pivot))
if (count != -1 && count < findings.length) return findings.slice(0, count)
return findings
}
export function parser(keys: KeyboardEvent[]) {
for (const { key } of keys) {
pushKey(key)
}
return { keys: [], ex_str: "", isMatch: true }
function createHighlightingElement(rect) {
let e = document.createElement("div")
e.className = "TridactylSearchHighlight"
e.style.display = "block"
e.style.position = "absolute"
e.style.top = rect.top + "px"
e.style.left = rect.left + "px"
e.style.width = rect.right - rect.left + "px"
e.style.height = rect.bottom - rect.top + "px"
return e
}
export function removeHighlighting(all = true) {
if (all) browserBg.find.removeHighlighting()
highlightingElements.forEach(e => e.parentNode.removeChild(e))
highlightingElements = []
}
/* Scrolls to the first visible node.
* i is the id of the node that should be scrolled to in allMatches
* direction is +1 if going forward and -1 if going backawrd
*/
export function findVisibleNode(allMatches, i, direction) {
let match = allMatches[i]
let n = i
do {
while (!match.firstNode.ownerDocument.contains(match.firstNode)) {
n += direction
match = lastMatches[n]
if (n == i) return null
}
match.firstNode.parentNode.scrollIntoView()
} while (!DOM.isVisible(match.firstNode.parentNode))
return match
}
let lastMatch = 0
let highlightingElements = []
/* Jumps to the startingFromth dom node matching pattern */
export async function jumpToMatch(pattern, reverse, startingFrom) {
removeHighlighting(false)
let match
// When we already computed all the matches, don't recompute them
if (lastMatches[0] && lastMatches[0].rangeData.text == pattern)
match = lastMatches[startingFrom]
if (!match) {
lastMatches = getMatches(await find(pattern, -1, reverse))
match = lastMatches[startingFrom]
}
if (!match) return
// Note: using this function can cause bugs, see
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/find/highlightResults
// Ideally we should reimplement our own highlighting
browserBg.find.highlightResults()
match = findVisibleNode(lastMatches, startingFrom, reverse ? -1 : 1)
for (let rect of match.rectData.rectsAndTexts.rectList) {
let elem = createHighlightingElement(rect)
highlightingElements.push(elem)
document.body.appendChild(elem)
}
// Remember where we where and what actions we did. This is need for jumpToNextMatch
lastMatch = lastMatches.indexOf(match)
}
export function jumpToNextMatch(n: number) {
removeHighlighting(false)
browserBg.find.highlightResults()
let match = findVisibleNode(
lastMatches,
(n + lastMatch + lastMatches.length) % lastMatches.length,
n <= 0 ? -1 : 1,
)
for (let rect of match.rectData.rectsAndTexts.rectList) {
let elem = createHighlightingElement(rect)
highlightingElements.push(elem)
document.body.appendChild(elem)
}
lastMatch = lastMatches.indexOf(match)
}
import * as SELF from "@src/content/finding.ts"
Messaging.addListener("finding_content", Messaging.attributeCaller(SELF))

View file

@ -8,7 +8,6 @@ export type ModeName =
| "ignore"
| "gobble"
| "input"
| "find"
export class PrevInput {
inputId: string

View file

@ -932,26 +932,53 @@ export function scrollpage(n = 1) {
//#content_helper
import * as finding from "@src/content/finding"
/** Start find mode. Work in progress.
/**
* Rudimentary find mode, left unbound by default as we don't currently support `incsearch`. Suggested binds:
*
* @param direction - the direction to search in: 1 is forwards, -1 is backwards.
* bind / fillcmdline find
* bind ? fillcmdline find -?
* bind n findnext 1
* bind N findnext -1
* bind ,<Space> nohlsearch
*
* Argument: A string you want to search for.
*
* This function accepts two flags: `-?` to search from the bottom rather than the top and `-: n` to jump directly to the nth match.
*
* The behavior of this function is affected by the following setting:
*
* `findcase`: either "smart", "sensitive" or "insensitive". If "smart", find will be case-sensitive if the pattern contains uppercase letters.
*
* Known bugs: find will currently happily jump to a non-visible element, and pressing n or N without having searched for anything will cause an error.
*/
//#content
export function find(direction?: -1 | 1) {
throw new Error("Our find mode is currently broken. Please `unbind /` and use Firefox's default find mode on `/`")
if (direction === undefined) direction = 1
finding.findPage(direction)
export function find(...args: string[]) {
let flagpos = args.indexOf("-?")
let reverse = flagpos >= 0
if (reverse) args.splice(flagpos, 1)
flagpos = args.indexOf("-:")
let startingFrom = 0
if (flagpos >= 0) {
startingFrom = parseInt(args[flagpos + 1]) || 0
args.splice(flagpos, 2)
}
finding.jumpToMatch(args.join(" "), reverse, startingFrom)
}
/** Highlight the next occurence of the previously searched for word.
/** Jump to the next searched pattern.
*
* @param number - number of words to advance down the page (use 1 for next word, -1 for previous)
*
*/
//#content
export function findnext(n: number) {
finding.navigate(n)
finding.jumpToNextMatch(n)
}
//#content
export function clearsearchhighlight() {
finding.removeHighlighting()
}
/** @hidden */
@ -2368,8 +2395,6 @@ export function mode(mode: ModeName) {
// TODO: event emition on mode change.
if (mode === "hint") {
hint()
} else if (mode === "find") {
find()
} else {
contentState.mode = mode
}

View file

@ -217,10 +217,11 @@ class default_config {
s: "fillcmdline open search",
S: "fillcmdline tabopen search",
// find mode not suitable for general consumption yet.
// "/": "find",
// "?": "find -1",
// "n": "findnext 1",
// "N": "findnext -1",
// "/": "fillcmdline find",
// "?": "fillcmdline find -?",
// n: "findnext 1",
// N: "findnext -1",
//",<Space>": "nohlsearch",
M: "gobble 1 quickmark",
B: "fillcmdline taball",
b: "fillcmdline tab",
@ -408,6 +409,8 @@ class default_config {
audel: "autocmddelete",
audelete: "autocmddelete",
b: "tab",
clsh: "clearsearchhighlight",
nohlsearch: "clearsearchhighlight",
o: "open",
w: "winopen",
t: "tabopen",
@ -775,6 +778,31 @@ class default_config {
*/
historyresults = 50
/**
* Number of results that should be shown in completions. -1 for unlimited
*/
findresults = -1
/**
* Number of characters to use as context for the matches shown in completions
*/
findcontextlen = 100
/**
* Whether find should be case-sensitive
*/
findcase: "smart" | "sensitive" | "insensitive" = "smart"
/**
* Whether Tridactyl should jump to the first match when using `:find`
*/
incsearch: "true" | "false" = "false"
/**
* How many characters should be typed before triggering incsearch/completions
*/
minincsearchlen = 3
/**
* Change this to "clobber" to ruin the "Content Security Policy" of all sites a bit and make Tridactyl run a bit better on some of them, e.g. raw.github*
*/

View file

@ -5,6 +5,7 @@ const logger = new Logger("messaging")
export type TabMessageType =
| "excmd_content"
| "commandline_content"
| "finding_content"
| "commandline_frame"
export type NonTabMessageType =
| "owntab_background"

View file

@ -48,6 +48,7 @@
"clipboardWrite",
"clipboardRead",
"downloads",
"find",
"history",
"sessions",
"storage",
@ -69,4 +70,4 @@
"page": "static/docs/classes/_src_lib_config_.default_config.html",
"open_in_tab": true
}
}
}

View file

@ -133,6 +133,10 @@ a.url:hover {
}
}
.FindCompletionOption .match {
font-weight: bold;
}
.hidden {
display: none;
}

View file

@ -50,3 +50,9 @@
display: none !important;
}
}
.TridactylSearchHighlight {
opacity: 0.5 !important;
background-color: var(--tridactyl-search-highlight-color) !important;
z-index: 2147483647 !important;
}

View file

@ -16,6 +16,9 @@
--tridactyl-status-border: 1px lightgray solid;
--tridactyl-status-border-radius: 2px;
/* Search highlight */
--tridactyl-search-highlight-color: yellow;
/* Hinting */
/* Hint character tags */

23
src/tridactyl.d.ts vendored
View file

@ -33,6 +33,29 @@ interface UIEvent {
pageY: number
}
// This isn't an actual firefox type but it's nice to have one for this kind of object
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/find/find
interface findResult {
count: number
rangeData: {
framePos: number
startTextNodePos: number
endTextNodePos: number
startOffset: number
endOffset: number
text: string
}[]
rectData: {
rectsAndTexts: {
top: number
left: number
bottom: number
right: number
}[]
textList: string[]
}
}
interface HTMLElement {
// Let's be future proof:
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus