mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 17:41:40 -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": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -4935,17 +4937,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -5062,7 +5067,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -5074,6 +5080,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -5088,6 +5095,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
@ -5095,12 +5103,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -5119,6 +5129,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -5199,7 +5210,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -5211,6 +5223,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -5332,6 +5345,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^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/number.clamp"
|
||||||
import "@src/lib/html-tagged-template"
|
import "@src/lib/html-tagged-template"
|
||||||
import * as Completions from "@src/completions"
|
import * as Completions from "@src/completions"
|
||||||
|
import { FindCompletionSource } from "./completions/Find"
|
||||||
import { TabAllCompletionSource } from "@src/completions/TabAll"
|
import { TabAllCompletionSource } from "@src/completions/TabAll"
|
||||||
import { BufferCompletionSource } from "@src/completions/Tab"
|
import { BufferCompletionSource } from "@src/completions/Tab"
|
||||||
import { BmarkCompletionSource } from "@src/completions/Bmark"
|
import { BmarkCompletionSource } from "@src/completions/Bmark"
|
||||||
|
@ -94,6 +95,7 @@ function getCompletion() {
|
||||||
export function enableCompletions() {
|
export function enableCompletions() {
|
||||||
if (!activeCompletions) {
|
if (!activeCompletions) {
|
||||||
activeCompletions = [
|
activeCompletions = [
|
||||||
|
// FindCompletionSource,
|
||||||
BmarkCompletionSource,
|
BmarkCompletionSource,
|
||||||
TabAllCompletionSource,
|
TabAllCompletionSource,
|
||||||
BufferCompletionSource,
|
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,
|
config,
|
||||||
dom,
|
dom,
|
||||||
excmds,
|
excmds,
|
||||||
hinting_content,
|
|
||||||
finding_content,
|
finding_content,
|
||||||
|
hinting_content,
|
||||||
itertools,
|
itertools,
|
||||||
logger,
|
logger,
|
||||||
Mark,
|
Mark,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import * as messaging from "@src/lib/messaging"
|
||||||
|
|
||||||
import { parser as exmode_parser } from "@src/parsers/exmode"
|
import { parser as exmode_parser } from "@src/parsers/exmode"
|
||||||
import * as hinting from "@src/content/hinting"
|
import * as hinting from "@src/content/hinting"
|
||||||
import * as finding from "@src/content/finding"
|
|
||||||
import * as gobblemode from "@src/parsers/gobblemode"
|
import * as gobblemode from "@src/parsers/gobblemode"
|
||||||
import * as generic from "@src/parsers/genericmode"
|
import * as generic from "@src/parsers/genericmode"
|
||||||
|
|
||||||
|
@ -47,7 +46,6 @@ function* ParserController() {
|
||||||
input: keys => generic.parser("inputmaps", keys),
|
input: keys => generic.parser("inputmaps", keys),
|
||||||
ignore: keys => generic.parser("ignoremaps", keys),
|
ignore: keys => generic.parser("ignoremaps", keys),
|
||||||
hint: hinting.parser,
|
hint: hinting.parser,
|
||||||
find: finding.parser,
|
|
||||||
gobble: gobblemode.parser,
|
gobble: gobblemode.parser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,8 +67,7 @@ function* ParserController() {
|
||||||
if (
|
if (
|
||||||
currentMode !== "ignore" &&
|
currentMode !== "ignore" &&
|
||||||
currentMode !== "hint" &&
|
currentMode !== "hint" &&
|
||||||
currentMode !== "input" &&
|
currentMode !== "input"
|
||||||
currentMode !== "find"
|
|
||||||
) {
|
) {
|
||||||
if (textEditable) {
|
if (textEditable) {
|
||||||
if (currentMode !== "insert") {
|
if (currentMode !== "insert") {
|
||||||
|
|
|
@ -1,157 +1,266 @@
|
||||||
/** Find mode.
|
// This file has various utilities used by the completion source for the find excmd
|
||||||
|
import * as Messaging from "@src/lib/messaging"
|
||||||
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"
|
|
||||||
import * as config from "@src/lib/config"
|
import * as config from "@src/lib/config"
|
||||||
import Logger from "@src/lib/logging"
|
import * as DOM from "@src/lib/dom"
|
||||||
import Mark from "mark.js"
|
import { zip } from "@src/lib/itertools"
|
||||||
const logger = new Logger("finding")
|
import { browserBg, activeTabId } from "@src/lib/webext"
|
||||||
|
|
||||||
function elementswithtext() {
|
export class Match {
|
||||||
return DOM.getElemsBySelector(
|
constructor(
|
||||||
"*",
|
public index,
|
||||||
// offsetHeight tells us which elements are drawn
|
public rangeData,
|
||||||
[
|
public rectData,
|
||||||
hint => {
|
public precontext,
|
||||||
return (
|
public postcontext,
|
||||||
(hint as any).offsetHeight > 0 &&
|
public firstNode,
|
||||||
(hint as any).offsetHeight !== undefined
|
) {}
|
||||||
)
|
}
|
||||||
},
|
|
||||||
hint => {
|
function isCommandLineNode(n) {
|
||||||
return hint.textContent != ""
|
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. */
|
/** Get all text nodes within the page.
|
||||||
class findState {
|
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.
|
||||||
readonly findHost = document.createElement("div")
|
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. */
|
||||||
public mark = new Mark(elementswithtext())
|
function getNodes() {
|
||||||
// ^ why does filtering by offsetHeight NOT work here
|
let nodes = []
|
||||||
public markedels = []
|
let walker = document.createTreeWalker(
|
||||||
public markpos = 0
|
document,
|
||||||
public direction: 1 | -1 = 1
|
NodeFilter.SHOW_TEXT,
|
||||||
constructor() {
|
null,
|
||||||
this.findHost.classList.add("TridactylfindHost")
|
false,
|
||||||
}
|
)
|
||||||
public filter = ""
|
let node = walker.nextNode()
|
||||||
|
do {
|
||||||
|
nodes.push(node)
|
||||||
|
node = walker.nextNode()
|
||||||
|
} while (node)
|
||||||
|
|
||||||
destructor() {
|
return nodes
|
||||||
// Remove all finds from the DOM.
|
|
||||||
this.findHost.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 */
|
// Checks if a node belongs to the command line
|
||||||
function filter(fstr) {
|
let nodes = getNodes()
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove all finds, reset STATE. */
|
for (let i = 0; i < findings.length; ++i) {
|
||||||
function reset(args?) {
|
let range = findings[i][0]
|
||||||
if (args.leavemarks == "false") findModeState.mark.unmark()
|
let firstnode = nodes[range.startTextNodePos]
|
||||||
if (args.unfind == "true") {
|
let lastnode = nodes[range.endTextNodePos]
|
||||||
findModeState.mark.unmark()
|
// We never want to match against nodes in the command line
|
||||||
findModeState.destructor()
|
if (
|
||||||
findModeState = undefined
|
!firstnode ||
|
||||||
}
|
!lastnode ||
|
||||||
contentState.mode = "normal"
|
isCommandLineNode(firstnode) ||
|
||||||
}
|
isCommandLineNode(lastnode)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
function mode(mode: "nav" | "search") {
|
// Get the context before the match
|
||||||
if (mode == "nav") {
|
let precontext = firstnode.textContent.substring(
|
||||||
// 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)
|
range.startOffset - contextLength,
|
||||||
findModeState.markedels = Array.from(
|
range.startOffset,
|
||||||
window.document.getElementsByTagName("mark"),
|
)
|
||||||
).filter(el => el.offsetHeight > 0)
|
if (precontext.length < contextLength) {
|
||||||
// ^ why does filtering by offsetHeight work here
|
let missingChars = contextLength - precontext.length
|
||||||
findModeState.markpos = 0
|
let id = range.startTextNodePos - 1
|
||||||
let el = findModeState.markedels[0]
|
while (missingChars > 0 && nodes[id]) {
|
||||||
if (el) {
|
let txt = nodes[id].textContent
|
||||||
if (!DOM.isVisible(el)) el.scrollIntoView()
|
precontext =
|
||||||
// colour of the selected link
|
txt.substring(txt.length - missingChars, txt.length) +
|
||||||
el.style.background = "lawngreen"
|
precontext
|
||||||
} else {
|
missingChars = contextLength - precontext.length
|
||||||
messageActiveTab("commandline_frame", "fillcmdline", [
|
id -= 1
|
||||||
"# Couldn't find pattern: " + findModeState.filter,
|
}
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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"
|
let prevFind = null
|
||||||
export function navigate(n: number = 1) {
|
let findCount = 0
|
||||||
// also - really - should probably actually make this be an excmd
|
/** 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.
|
||||||
// people will want to be able to scroll and stuff.
|
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. */
|
||||||
// should probably move this to an update function?
|
export async function find(query, count = -1, reverse = false) {
|
||||||
// don't hardcode this colour
|
findCount += 1
|
||||||
findModeState.markedels[findModeState.markpos].style.background = "yellow"
|
let findId = findCount
|
||||||
findModeState.markpos = (
|
let findcase = await config.getAsync("findcase")
|
||||||
findModeState.markpos +
|
let caseSensitive =
|
||||||
n * findModeState.direction
|
findcase == "sensitive" ||
|
||||||
).mod(findModeState.markedels.length)
|
(findcase == "smart" && query.search(/[A-Z]/) >= 0)
|
||||||
// obvs need to do mod to wrap indices
|
let tabId = await activeTabId()
|
||||||
let el = findModeState.markedels[findModeState.markpos]
|
|
||||||
if (!DOM.isVisible(el)) el.scrollIntoView()
|
|
||||||
el.style.background = "lawngreen"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findPage(direction?: 1 | -1) {
|
// No point in searching for something that won't be used anyway
|
||||||
if (findModeState !== undefined) reset({ unfind: "true" })
|
await prevFind
|
||||||
contentState.mode = "find"
|
if (findId != findCount) return []
|
||||||
findModeState = new findState()
|
|
||||||
if (direction !== undefined) findModeState.direction = direction
|
|
||||||
document.body.appendChild(findModeState.findHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If key is in findchars, add it to filtstr and filter */
|
prevFind = browserBg.find.find(query, {
|
||||||
function pushKey(ke) {
|
tabId,
|
||||||
if (ke.key === "Backspace") {
|
caseSensitive,
|
||||||
findModeState.filter = findModeState.filter.slice(0, -1)
|
includeRangeData: true,
|
||||||
filter(findModeState.filter)
|
includeRectData: true,
|
||||||
} else if (ke.key === "Enter") {
|
})
|
||||||
mode("nav")
|
let findings = await prevFind
|
||||||
reset({ leavemarks: "true" })
|
findings = zip(findings.rangeData, findings.rectData).sort(
|
||||||
} else if (ke.key === "Escape") {
|
(a: any, b: any) => {
|
||||||
reset({ unfind: "true" })
|
a = a[1].rectsAndTexts.rectList[0]
|
||||||
} else if (ke.key.length > 1) {
|
b = b[1].rectsAndTexts.rectList[0]
|
||||||
return
|
if (!a || !b) return 0
|
||||||
} else {
|
return a.top - b.top
|
||||||
findModeState.filter += ke.key
|
},
|
||||||
filter(findModeState.filter)
|
)
|
||||||
|
|
||||||
|
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[]) {
|
function createHighlightingElement(rect) {
|
||||||
for (const { key } of keys) {
|
let e = document.createElement("div")
|
||||||
pushKey(key)
|
e.className = "TridactylSearchHighlight"
|
||||||
}
|
e.style.display = "block"
|
||||||
return { keys: [], ex_str: "", isMatch: true }
|
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"
|
| "ignore"
|
||||||
| "gobble"
|
| "gobble"
|
||||||
| "input"
|
| "input"
|
||||||
| "find"
|
|
||||||
|
|
||||||
export class PrevInput {
|
export class PrevInput {
|
||||||
inputId: string
|
inputId: string
|
||||||
|
|
|
@ -932,26 +932,53 @@ export function scrollpage(n = 1) {
|
||||||
//#content_helper
|
//#content_helper
|
||||||
import * as finding from "@src/content/finding"
|
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
|
//#content
|
||||||
export function find(direction?: -1 | 1) {
|
export function find(...args: string[]) {
|
||||||
throw new Error("Our find mode is currently broken. Please `unbind /` and use Firefox's default find mode on `/`")
|
let flagpos = args.indexOf("-?")
|
||||||
if (direction === undefined) direction = 1
|
let reverse = flagpos >= 0
|
||||||
finding.findPage(direction)
|
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)
|
* @param number - number of words to advance down the page (use 1 for next word, -1 for previous)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
//#content
|
//#content
|
||||||
export function findnext(n: number) {
|
export function findnext(n: number) {
|
||||||
finding.navigate(n)
|
finding.jumpToNextMatch(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#content
|
||||||
|
export function clearsearchhighlight() {
|
||||||
|
finding.removeHighlighting()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
@ -2368,8 +2395,6 @@ export function mode(mode: ModeName) {
|
||||||
// TODO: event emition on mode change.
|
// TODO: event emition on mode change.
|
||||||
if (mode === "hint") {
|
if (mode === "hint") {
|
||||||
hint()
|
hint()
|
||||||
} else if (mode === "find") {
|
|
||||||
find()
|
|
||||||
} else {
|
} else {
|
||||||
contentState.mode = mode
|
contentState.mode = mode
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,10 +217,11 @@ class default_config {
|
||||||
s: "fillcmdline open search",
|
s: "fillcmdline open search",
|
||||||
S: "fillcmdline tabopen search",
|
S: "fillcmdline tabopen search",
|
||||||
// find mode not suitable for general consumption yet.
|
// find mode not suitable for general consumption yet.
|
||||||
// "/": "find",
|
// "/": "fillcmdline find",
|
||||||
// "?": "find -1",
|
// "?": "fillcmdline find -?",
|
||||||
// "n": "findnext 1",
|
// n: "findnext 1",
|
||||||
// "N": "findnext -1",
|
// N: "findnext -1",
|
||||||
|
//",<Space>": "nohlsearch",
|
||||||
M: "gobble 1 quickmark",
|
M: "gobble 1 quickmark",
|
||||||
B: "fillcmdline taball",
|
B: "fillcmdline taball",
|
||||||
b: "fillcmdline tab",
|
b: "fillcmdline tab",
|
||||||
|
@ -408,6 +409,8 @@ class default_config {
|
||||||
audel: "autocmddelete",
|
audel: "autocmddelete",
|
||||||
audelete: "autocmddelete",
|
audelete: "autocmddelete",
|
||||||
b: "tab",
|
b: "tab",
|
||||||
|
clsh: "clearsearchhighlight",
|
||||||
|
nohlsearch: "clearsearchhighlight",
|
||||||
o: "open",
|
o: "open",
|
||||||
w: "winopen",
|
w: "winopen",
|
||||||
t: "tabopen",
|
t: "tabopen",
|
||||||
|
@ -775,6 +778,31 @@ class default_config {
|
||||||
*/
|
*/
|
||||||
historyresults = 50
|
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*
|
* 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 =
|
export type TabMessageType =
|
||||||
| "excmd_content"
|
| "excmd_content"
|
||||||
| "commandline_content"
|
| "commandline_content"
|
||||||
|
| "finding_content"
|
||||||
| "commandline_frame"
|
| "commandline_frame"
|
||||||
export type NonTabMessageType =
|
export type NonTabMessageType =
|
||||||
| "owntab_background"
|
| "owntab_background"
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"clipboardRead",
|
"clipboardRead",
|
||||||
"downloads",
|
"downloads",
|
||||||
|
"find",
|
||||||
"history",
|
"history",
|
||||||
"sessions",
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
|
@ -69,4 +70,4 @@
|
||||||
"page": "static/docs/classes/_src_lib_config_.default_config.html",
|
"page": "static/docs/classes/_src_lib_config_.default_config.html",
|
||||||
"open_in_tab": true
|
"open_in_tab": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,10 @@ a.url:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.FindCompletionOption .match {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,3 +50,9 @@
|
||||||
display: none !important;
|
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: 1px lightgray solid;
|
||||||
--tridactyl-status-border-radius: 2px;
|
--tridactyl-status-border-radius: 2px;
|
||||||
|
|
||||||
|
/* Search highlight */
|
||||||
|
--tridactyl-search-highlight-color: yellow;
|
||||||
|
|
||||||
/* Hinting */
|
/* Hinting */
|
||||||
|
|
||||||
/* Hint character tags */
|
/* Hint character tags */
|
||||||
|
|
23
src/tridactyl.d.ts
vendored
23
src/tridactyl.d.ts
vendored
|
@ -33,6 +33,29 @@ interface UIEvent {
|
||||||
pageY: number
|
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 {
|
interface HTMLElement {
|
||||||
// Let's be future proof:
|
// Let's be future proof:
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
||||||
|
|
Loading…
Add table
Reference in a new issue