mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 17:41:40 -05:00
Find.ts: Implement :find, :findnext
This commit is contained in:
parent
b3730c0801
commit
b8f7d5f389
5 changed files with 248 additions and 61 deletions
|
@ -7,16 +7,17 @@ import * as config from "../config"
|
|||
class FindCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
constructor(m) {
|
||||
constructor(m, reverse = false) {
|
||||
super()
|
||||
this.value = m.rangeData.text
|
||||
this.fuseKeys.push(this.value)
|
||||
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">${
|
||||
this.value
|
||||
m.rangeData.text
|
||||
}</span>${m.postcontext}</td>
|
||||
</tr>`
|
||||
}
|
||||
|
@ -28,7 +29,7 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
|||
public completionCount = 0
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["find"], "FindCompletionSource", "Matches")
|
||||
super(["find "], "FindCompletionSource", "Matches")
|
||||
|
||||
this.updateOptions()
|
||||
this._parent.appendChild(this.node)
|
||||
|
@ -48,26 +49,35 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
private async updateOptions(exstr?: string) {
|
||||
if (!exstr) return
|
||||
let query = exstr.substring(exstr.trim().indexOf(" ") + 1)
|
||||
if (!query || query == 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(" ")
|
||||
// No point if continuing if the user hasn't started searching yet
|
||||
if (query.length == 0) return
|
||||
|
||||
let findresults = await config.getAsync("findresults")
|
||||
if (findresults === 0) return
|
||||
let findcase = await config.getAsync("findcase")
|
||||
let caseSensitive =
|
||||
findcase == "sensitive" ||
|
||||
(findcase == "smart" && query.search(/[A-Z]/) >= 0)
|
||||
|
||||
this.lastExstr = exstr
|
||||
// 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 = browserBg.find.find(query, {
|
||||
let findings = await Messaging.messageTab(
|
||||
tabId,
|
||||
caseSensitive,
|
||||
includeRangeData: true,
|
||||
})
|
||||
findings = await findings
|
||||
"finding_content",
|
||||
"find",
|
||||
[query, findresults, reverse],
|
||||
)
|
||||
|
||||
// If the search was successful
|
||||
if (findings.count > 0) {
|
||||
if (findresults != -1 && findresults < findings.count)
|
||||
findings.count = findresults
|
||||
// Get match context
|
||||
let len = await config.getAsync("findcontextlen")
|
||||
let matches = await Messaging.messageTab(
|
||||
tabId,
|
||||
|
@ -75,9 +85,11 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
|||
"getMatches",
|
||||
[findings, len],
|
||||
)
|
||||
this.options = matches.map(m => new FindCompletionOption(m))
|
||||
|
||||
this.options = matches.map(
|
||||
m => new FindCompletionOption(m, reverse),
|
||||
)
|
||||
this.updateChain(exstr, this.options)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,11 +98,10 @@ const DEFAULTS = o({
|
|||
":": "fillcmdline",
|
||||
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",
|
||||
M: "gobble 1 quickmark",
|
||||
B: "fillcmdline bufferall",
|
||||
b: "fillcmdline buffer",
|
||||
|
@ -161,6 +160,7 @@ const DEFAULTS = o({
|
|||
alias: "command",
|
||||
au: "autocmd",
|
||||
b: "buffer",
|
||||
clsh: "clearsearchhighlight",
|
||||
o: "open",
|
||||
w: "winopen",
|
||||
t: "tabopen",
|
||||
|
|
|
@ -751,14 +751,27 @@ export function scrollpage(n = 1) {
|
|||
scrollpx(0, window.innerHeight * n)
|
||||
}
|
||||
|
||||
//#content_helper
|
||||
import * as finding from "./finding_content"
|
||||
|
||||
/** Start find mode. Work in progress.
|
||||
*
|
||||
* @param direction - the direction to search in: 1 is forwards, -1 is backwards.
|
||||
*
|
||||
*/
|
||||
//#background
|
||||
export function find(str: string) {
|
||||
console.log("find ", str)
|
||||
//#content
|
||||
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.
|
||||
|
@ -766,9 +779,14 @@ export function find(str: string) {
|
|||
* @param number - number of words to advance down the page (use 1 for next word, -1 for previous)
|
||||
*
|
||||
*/
|
||||
//#background
|
||||
//#content
|
||||
export function findnext(n: number) {
|
||||
console.log("findnext ", n)
|
||||
finding.jumpToNextMatch(n)
|
||||
}
|
||||
|
||||
//#background
|
||||
export function clearsearchhighlight() {
|
||||
browserBg.find.removeHighlighting()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
|
|
|
@ -1,85 +1,219 @@
|
|||
// This file has various utilities used by the completion source for the find excmd
|
||||
import * as Messaging from "./messaging"
|
||||
import * as config from "./config"
|
||||
import { browserBg, activeTabId } from "./lib/webext"
|
||||
|
||||
export class Match {
|
||||
constructor(public rangeData, public rectData, public precontext, public postcontext){}
|
||||
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"
|
||||
return (
|
||||
url.protocol == "moz-extension:" &&
|
||||
url.pathname == "/static/commandline.html"
|
||||
)
|
||||
}
|
||||
|
||||
// We cache these results because walking the tree can be quite expensive when there's a lot of nodes
|
||||
// TODO: Implement cache invalidation using a MutationObserver to track nodes being added to or removed from the DOM
|
||||
let nodes = null
|
||||
/** 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() {
|
||||
// if (nodes != null)
|
||||
// return nodes
|
||||
nodes = []
|
||||
let walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, null, false);
|
||||
let nodes = []
|
||||
let walker = document.createTreeWalker(
|
||||
document,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
let node = walker.nextNode()
|
||||
do {
|
||||
nodes.push(node);
|
||||
nodes.push(node)
|
||||
node = walker.nextNode()
|
||||
} while (node)
|
||||
|
||||
return nodes;
|
||||
return nodes
|
||||
}
|
||||
|
||||
/** Given "findings", an object matching the ones returned by browser.find.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 from before and after the match should be included into the returned Match object */
|
||||
let lastMatches = []
|
||||
/** Given "findings", an object matching the ones returned by browser.find.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 = []
|
||||
|
||||
if (findings.count == 0)
|
||||
return result
|
||||
if (findings.count == 0) return result
|
||||
|
||||
if (!findings.rangeData)
|
||||
throw new Error("Can't get matches without range data!")
|
||||
|
||||
// Helper function to create matches. This avoids a `if` in the loop below
|
||||
let constructMatch = (findings, i, precontext, postcontext) => new Match(findings.rangeData[i], findings.rectData[i], precontext, postcontext)
|
||||
let constructMatch = (findings, i, precontext, postcontext, node) =>
|
||||
new Match(
|
||||
i,
|
||||
findings.rangeData[i],
|
||||
findings.rectData[i],
|
||||
precontext,
|
||||
postcontext,
|
||||
node,
|
||||
)
|
||||
if (!findings.rectData)
|
||||
constructMatch = (findings, i, precontext, postcontext) => new Match(findings.rangeData[i], null, precontext, postcontext)
|
||||
constructMatch = (findings, i, precontext, postcontext, node) =>
|
||||
new Match(
|
||||
i,
|
||||
findings.rangeData[i],
|
||||
null,
|
||||
precontext,
|
||||
postcontext,
|
||||
node,
|
||||
)
|
||||
|
||||
// Checks if a node belongs to the command line
|
||||
let nodes = getNodes();
|
||||
let nodes = getNodes()
|
||||
|
||||
for (let i = 0; i < findings.count; ++i) {
|
||||
let range = findings.rangeData[i]
|
||||
let firstnode = nodes[range.startTextNodePos];
|
||||
let lastnode = nodes[range.endTextNodePos];
|
||||
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)) {
|
||||
console.log(i, nodes, range, firstnode, lastnode)
|
||||
if (
|
||||
!firstnode ||
|
||||
!lastnode ||
|
||||
isCommandLineNode(firstnode) ||
|
||||
isCommandLineNode(lastnode)
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the context before the match
|
||||
let precontext = firstnode.textContent.substring(range.startOffset - contextLength, range.startOffset)
|
||||
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
|
||||
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)
|
||||
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]) {
|
||||
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)
|
||||
postcontext += nodes[range.endTextNodePos + 1].textContent.substr(
|
||||
0,
|
||||
contextLength - postcontext.length,
|
||||
)
|
||||
}
|
||||
|
||||
result.push(constructMatch(findings, i, precontext, postcontext));
|
||||
|
||||
result.push(
|
||||
constructMatch(findings, i, precontext, postcontext, firstnode),
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
if (cachedQuery != result[0].rangeData.text) matchesCacheIsValid = false
|
||||
return result
|
||||
}
|
||||
|
||||
let matchesCacheIsValid = false
|
||||
let cachedQuery = ""
|
||||
/** Performs a call to browser.find.find() with the right parameters.
|
||||
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,
|
||||
): Promise<findResult> {
|
||||
let findcase = await config.getAsync("findcase")
|
||||
let caseSensitive =
|
||||
findcase == "sensitive" ||
|
||||
(findcase == "smart" && query.search(/[A-Z]/) >= 0)
|
||||
let tabId = await activeTabId()
|
||||
let findings = await browserBg.find.find(query, {
|
||||
tabId,
|
||||
caseSensitive,
|
||||
includeRangeData: true,
|
||||
includeRectData: true,
|
||||
})
|
||||
if (reverse) {
|
||||
findings.rangeData = findings.rangeData.reverse()
|
||||
findings.rectData = findings.rectData.reverse()
|
||||
}
|
||||
if (count != -1 && count < findings.count) {
|
||||
findings.count = count
|
||||
findings.rangeData = findings.rangeData.slice(0, findings.count)
|
||||
findings.rectData = findings.rectData.slice(0, findings.count)
|
||||
matchesCacheIsValid = false
|
||||
} else {
|
||||
matchesCacheIsValid = true
|
||||
}
|
||||
cachedQuery = query
|
||||
return findings
|
||||
}
|
||||
|
||||
let lastMatch = 0
|
||||
let lastReverse = false
|
||||
/* Jumps to the startingFromth dom node matching pattern */
|
||||
export async function jumpToMatch(pattern, reverse, startingFrom) {
|
||||
let match
|
||||
|
||||
// When we already computed all the matches, don't recompute them
|
||||
if (matchesCacheIsValid && pattern == cachedQuery)
|
||||
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()
|
||||
|
||||
// Here, we make sure that the current match hasn't been removed from its owner document
|
||||
// If it has, we try to find the next/previous node that is still in the document
|
||||
let n = startingFrom
|
||||
while (!match.firstNode.ownerDocument.contains(match.firstNode)) {
|
||||
n += reverse ? -1 : 1
|
||||
match = lastMatches[startingFrom]
|
||||
if (n == startingFrom) return
|
||||
}
|
||||
|
||||
match.firstNode.parentNode.scrollIntoView()
|
||||
// Remember where we where and what actions we did. This is need for jumpToNextMatch
|
||||
lastMatch = n
|
||||
lastReverse = reverse
|
||||
}
|
||||
|
||||
export function jumpToNextMatch(n: number) {
|
||||
if (lastReverse) n *= -1
|
||||
n = (n + lastMatch + lastMatches.length) % lastMatches.length
|
||||
let match = lastMatches[n]
|
||||
browserBg.find.highlightResults()
|
||||
match.firstNode.parentNode.scrollIntoView()
|
||||
lastMatch = n
|
||||
}
|
||||
|
||||
import * as SELF from "./finding_content"
|
||||
|
|
23
src/tridactyl.d.ts
vendored
23
src/tridactyl.d.ts
vendored
|
@ -22,6 +22,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