mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 09:31:41 -05:00
Merge branch 'make_find_faster'
This commit is contained in:
commit
ba0dea0349
15 changed files with 493 additions and 163 deletions
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
118
src/completions/Find.ts
Normal 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"))
|
||||
}
|
||||
}
|
|
@ -66,8 +66,8 @@ import * as styling from "@src/content/styling"
|
|||
config,
|
||||
dom,
|
||||
excmds,
|
||||
hinting_content,
|
||||
finding_content,
|
||||
hinting_content,
|
||||
itertools,
|
||||
logger,
|
||||
Mark,
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -8,7 +8,6 @@ export type ModeName =
|
|||
| "ignore"
|
||||
| "gobble"
|
||||
| "input"
|
||||
| "find"
|
||||
|
||||
export class PrevInput {
|
||||
inputId: string
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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*
|
||||
*/
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,10 @@ a.url:hover {
|
|||
}
|
||||
}
|
||||
|
||||
.FindCompletionOption .match {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -50,3 +50,9 @@
|
|||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.TridactylSearchHighlight {
|
||||
opacity: 0.5 !important;
|
||||
background-color: var(--tridactyl-search-highlight-color) !important;
|
||||
z-index: 2147483647 !important;
|
||||
}
|
||||
|
|
|
@ -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
23
src/tridactyl.d.ts
vendored
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue