mirror of
https://github.com/vale981/tridactyl
synced 2025-03-06 01:51: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
|
class FindCompletionOption extends Completions.CompletionOptionHTML
|
||||||
implements Completions.CompletionOptionFuse {
|
implements Completions.CompletionOptionFuse {
|
||||||
public fuseKeys = []
|
public fuseKeys = []
|
||||||
constructor(m) {
|
constructor(m, reverse = false) {
|
||||||
super()
|
super()
|
||||||
this.value = m.rangeData.text
|
this.value =
|
||||||
this.fuseKeys.push(this.value)
|
(reverse ? "-? " : "") + ("-: " + m.index) + " " + m.rangeData.text
|
||||||
|
this.fuseKeys.push(m.rangeData.text)
|
||||||
|
|
||||||
let contextLength = 4
|
let contextLength = 4
|
||||||
// Create HTMLElement
|
// Create HTMLElement
|
||||||
this.html = html`<tr class="FindCompletionOption option">
|
this.html = html`<tr class="FindCompletionOption option">
|
||||||
<td class="content">${m.precontext}<span class="match">${
|
<td class="content">${m.precontext}<span class="match">${
|
||||||
this.value
|
m.rangeData.text
|
||||||
}</span>${m.postcontext}</td>
|
}</span>${m.postcontext}</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
||||||
public completionCount = 0
|
public completionCount = 0
|
||||||
|
|
||||||
constructor(private _parent) {
|
constructor(private _parent) {
|
||||||
super(["find"], "FindCompletionSource", "Matches")
|
super(["find "], "FindCompletionSource", "Matches")
|
||||||
|
|
||||||
this.updateOptions()
|
this.updateOptions()
|
||||||
this._parent.appendChild(this.node)
|
this._parent.appendChild(this.node)
|
||||||
|
@ -48,26 +49,35 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
||||||
|
|
||||||
private async updateOptions(exstr?: string) {
|
private async updateOptions(exstr?: string) {
|
||||||
if (!exstr) return
|
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")
|
let findresults = await config.getAsync("findresults")
|
||||||
if (findresults === 0) return
|
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 tabId = await activeTabId()
|
||||||
let findings = browserBg.find.find(query, {
|
let findings = await Messaging.messageTab(
|
||||||
tabId,
|
tabId,
|
||||||
caseSensitive,
|
"finding_content",
|
||||||
includeRangeData: true,
|
"find",
|
||||||
})
|
[query, findresults, reverse],
|
||||||
findings = await findings
|
)
|
||||||
|
|
||||||
|
// If the search was successful
|
||||||
if (findings.count > 0) {
|
if (findings.count > 0) {
|
||||||
if (findresults != -1 && findresults < findings.count)
|
// Get match context
|
||||||
findings.count = findresults
|
|
||||||
let len = await config.getAsync("findcontextlen")
|
let len = await config.getAsync("findcontextlen")
|
||||||
let matches = await Messaging.messageTab(
|
let matches = await Messaging.messageTab(
|
||||||
tabId,
|
tabId,
|
||||||
|
@ -75,9 +85,11 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
||||||
"getMatches",
|
"getMatches",
|
||||||
[findings, len],
|
[findings, len],
|
||||||
)
|
)
|
||||||
this.options = matches.map(m => new FindCompletionOption(m))
|
|
||||||
|
this.options = matches.map(
|
||||||
|
m => new FindCompletionOption(m, reverse),
|
||||||
|
)
|
||||||
this.updateChain(exstr, this.options)
|
this.updateChain(exstr, this.options)
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,11 +98,10 @@ const DEFAULTS = o({
|
||||||
":": "fillcmdline",
|
":": "fillcmdline",
|
||||||
s: "fillcmdline open search",
|
s: "fillcmdline open search",
|
||||||
S: "fillcmdline tabopen search",
|
S: "fillcmdline tabopen search",
|
||||||
// find mode not suitable for general consumption yet.
|
"/": "fillcmdline find",
|
||||||
// "/": "find",
|
"?": "fillcmdline find -?",
|
||||||
// "?": "find -1",
|
n: "findnext 1",
|
||||||
// "n": "findnext 1",
|
N: "findnext -1",
|
||||||
// "N": "findnext -1",
|
|
||||||
M: "gobble 1 quickmark",
|
M: "gobble 1 quickmark",
|
||||||
B: "fillcmdline bufferall",
|
B: "fillcmdline bufferall",
|
||||||
b: "fillcmdline buffer",
|
b: "fillcmdline buffer",
|
||||||
|
@ -161,6 +160,7 @@ const DEFAULTS = o({
|
||||||
alias: "command",
|
alias: "command",
|
||||||
au: "autocmd",
|
au: "autocmd",
|
||||||
b: "buffer",
|
b: "buffer",
|
||||||
|
clsh: "clearsearchhighlight",
|
||||||
o: "open",
|
o: "open",
|
||||||
w: "winopen",
|
w: "winopen",
|
||||||
t: "tabopen",
|
t: "tabopen",
|
||||||
|
|
|
@ -751,14 +751,27 @@ export function scrollpage(n = 1) {
|
||||||
scrollpx(0, window.innerHeight * n)
|
scrollpx(0, window.innerHeight * n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#content_helper
|
||||||
|
import * as finding from "./finding_content"
|
||||||
|
|
||||||
/** Start find mode. Work in progress.
|
/** Start find mode. Work in progress.
|
||||||
*
|
*
|
||||||
* @param direction - the direction to search in: 1 is forwards, -1 is backwards.
|
* @param direction - the direction to search in: 1 is forwards, -1 is backwards.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
//#background
|
//#content
|
||||||
export function find(str: string) {
|
export function find(...args: string[]) {
|
||||||
console.log("find ", str)
|
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.
|
/** 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)
|
* @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) {
|
export function findnext(n: number) {
|
||||||
console.log("findnext ", n)
|
finding.jumpToNextMatch(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#background
|
||||||
|
export function clearsearchhighlight() {
|
||||||
|
browserBg.find.removeHighlighting()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hidden */
|
/** @hidden */
|
||||||
|
|
|
@ -1,85 +1,219 @@
|
||||||
// This file has various utilities used by the completion source for the find excmd
|
// This file has various utilities used by the completion source for the find excmd
|
||||||
import * as Messaging from "./messaging"
|
import * as Messaging from "./messaging"
|
||||||
|
import * as config from "./config"
|
||||||
|
import { browserBg, activeTabId } from "./lib/webext"
|
||||||
|
|
||||||
export class Match {
|
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) {
|
function isCommandLineNode(n) {
|
||||||
let url = n.ownerDocument.location.href
|
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
|
/** Get all text nodes within the page.
|
||||||
// TODO: Implement cache invalidation using a MutationObserver to track nodes being added to or removed from the DOM
|
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.
|
||||||
let nodes = null
|
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() {
|
function getNodes() {
|
||||||
// if (nodes != null)
|
let nodes = []
|
||||||
// return nodes
|
let walker = document.createTreeWalker(
|
||||||
nodes = []
|
document,
|
||||||
let walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, null, false);
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
)
|
||||||
let node = walker.nextNode()
|
let node = walker.nextNode()
|
||||||
do {
|
do {
|
||||||
nodes.push(node);
|
nodes.push(node)
|
||||||
node = walker.nextNode()
|
node = walker.nextNode()
|
||||||
} while (node)
|
} 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[] {
|
export function getMatches(findings, contextLength = 10): Match[] {
|
||||||
let result = []
|
let result = []
|
||||||
|
|
||||||
if (findings.count == 0)
|
if (findings.count == 0) return result
|
||||||
return result
|
|
||||||
|
|
||||||
if (!findings.rangeData)
|
if (!findings.rangeData)
|
||||||
throw new Error("Can't get matches without range data!")
|
throw new Error("Can't get matches without range data!")
|
||||||
|
|
||||||
// Helper function to create matches. This avoids a `if` in the loop below
|
// 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)
|
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
|
// Checks if a node belongs to the command line
|
||||||
let nodes = getNodes();
|
let nodes = getNodes()
|
||||||
|
|
||||||
for (let i = 0; i < findings.count; ++i) {
|
for (let i = 0; i < findings.count; ++i) {
|
||||||
let range = findings.rangeData[i]
|
let range = findings.rangeData[i]
|
||||||
let firstnode = nodes[range.startTextNodePos];
|
let firstnode = nodes[range.startTextNodePos]
|
||||||
let lastnode = nodes[range.endTextNodePos];
|
let lastnode = nodes[range.endTextNodePos]
|
||||||
// We never want to match against nodes in the command line
|
// We never want to match against nodes in the command line
|
||||||
if (!firstnode || !lastnode || isCommandLineNode(firstnode) || isCommandLineNode(lastnode)) {
|
if (
|
||||||
console.log(i, nodes, range, firstnode, lastnode)
|
!firstnode ||
|
||||||
|
!lastnode ||
|
||||||
|
isCommandLineNode(firstnode) ||
|
||||||
|
isCommandLineNode(lastnode)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
// Get the context before the match
|
// 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) {
|
if (precontext.length < contextLength) {
|
||||||
let missingChars = contextLength - precontext.length
|
let missingChars = contextLength - precontext.length
|
||||||
let id = range.startTextNodePos - 1
|
let id = range.startTextNodePos - 1
|
||||||
while (missingChars > 0 && nodes[id]) {
|
while (missingChars > 0 && nodes[id]) {
|
||||||
let txt = nodes[id].textContent
|
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
|
missingChars = contextLength - precontext.length
|
||||||
id -= 1
|
id -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the context after the match
|
// 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 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
|
// 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"
|
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
|
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