mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 07:16:33 -04:00
Add :find completions back
Also improve perf: - index headings for breadcrumbs - don't clone ranges to make highlights - fiddle with debounce
This commit is contained in:
parent
e8d85058f7
commit
b5ba02e695
|
|
@ -114,6 +114,7 @@ function resizeArea() {
|
|||
focus()
|
||||
}
|
||||
}
|
||||
window.addEventListener("tridactyl-refresh-completions", resizeArea)
|
||||
|
||||
/** @hidden
|
||||
* This is a bit loosely defined at the moment.
|
||||
|
|
|
|||
|
|
@ -2,39 +2,160 @@ import * as Completions from "../completions"
|
|||
import * as Messaging from "@src/lib/messaging"
|
||||
import { ownTabId } from "@src/lib/webext"
|
||||
|
||||
class FindCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys
|
||||
|
||||
constructor(index: number, match, args: string) {
|
||||
super()
|
||||
this.value = [`--jump-to ${index}`, args].filter(Boolean).join(" ")
|
||||
this.fuseKeys = [match.text, match.precontext, match.postcontext, match.breadcrumbs]
|
||||
this.html = html`<tr class="FindCompletionOption option">
|
||||
<td class="breadcrumbs">${match.breadcrumbs}</td>
|
||||
<td class="content">
|
||||
${match.precontext}<span class="match">${match.text}</span
|
||||
>${match.postcontext}
|
||||
</td>
|
||||
<td class="position">${match.position}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
||||
export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options = []
|
||||
public options: FindCompletionOption[] = []
|
||||
private session = Math.random()
|
||||
private tabId = ownTabId()
|
||||
private active = false
|
||||
private request = 0
|
||||
private findArgs = ""
|
||||
private pending: Promise<void> = Promise.resolve()
|
||||
private selection: Promise<void> = Promise.resolve()
|
||||
|
||||
constructor(_parent?) {
|
||||
super(["find"], "FindCompletionSource")
|
||||
super(
|
||||
["find"],
|
||||
"FindCompletionSource",
|
||||
html`<table><tr>
|
||||
<td class="breadcrumbs">Breadcrumbs</td>
|
||||
<td class="content">Context</td>
|
||||
<td class="position">Position</td>
|
||||
</tr></table>`,
|
||||
)
|
||||
}
|
||||
|
||||
filter(exstr: string) {
|
||||
if (exstr === this.lastExstr && this.completion) return Promise.resolve()
|
||||
this.lastExstr = exstr
|
||||
const [, argstr] = this.splitOnPrefix(exstr)
|
||||
if (argstr === undefined) return this.cancel()
|
||||
const request = ++this.request
|
||||
this.findArgs = argstr.trim()
|
||||
this.active = true
|
||||
void this.send(
|
||||
{ session: this.session },
|
||||
...argstr.trim().split(/\s+/),
|
||||
).catch(() => undefined)
|
||||
this.options = []
|
||||
this.optionContainer.replaceChildren()
|
||||
this.state = "hidden"
|
||||
const optionArgs = this.findArgs.split(/(?:^|\s)--(?:\s|$)/, 1)[0]
|
||||
if (/(^|\s)(--jump-to|-:)=?$/.test(optionArgs)) return this.cancel()
|
||||
const hasJump = /(^|\s)(--jump-to(?:=|\s)|-:(?:\s|$))/.test(optionArgs)
|
||||
const regex = /(^|\s)(-r|--regex)(?=\s|$)/.test(optionArgs)
|
||||
const delay = regex
|
||||
? new Promise<void>(resolve => window.setTimeout(resolve, 250))
|
||||
: Promise.resolve()
|
||||
this.pending = delay
|
||||
.then(() => {
|
||||
if (request !== this.request) return
|
||||
return this.send(
|
||||
{ session: this.session, completions: !hasJump },
|
||||
...this.findArgs.split(/\s+/),
|
||||
)
|
||||
})
|
||||
.then(matches => {
|
||||
if (request !== this.request) return
|
||||
this.options = hasJump
|
||||
? []
|
||||
: (matches || []).map(
|
||||
match =>
|
||||
new FindCompletionOption(
|
||||
match.index,
|
||||
match,
|
||||
this.findArgs,
|
||||
),
|
||||
)
|
||||
if (this.options.length) this.updateChain(exstr, this.options)
|
||||
else this.state = "hidden"
|
||||
this.resize()
|
||||
})
|
||||
.catch(() => {
|
||||
if (request !== this.request) return
|
||||
this.options = []
|
||||
this.state = "hidden"
|
||||
this.resize()
|
||||
return this.send({ session: this.session, cancel: true })
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
setStateFromScore() {
|
||||
this.options.forEach(option => (option.state = "normal"))
|
||||
this.deselect()
|
||||
}
|
||||
|
||||
scoredOptions() {
|
||||
return []
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
this.optionContainer.replaceChildren(
|
||||
...this.options
|
||||
.filter(option => option.state !== "hidden")
|
||||
.map(option => option.html),
|
||||
)
|
||||
this.next(0)
|
||||
}
|
||||
|
||||
select(option: FindCompletionOption) {
|
||||
super.select(option)
|
||||
this.selection = this.preview(option.value, true)
|
||||
}
|
||||
|
||||
async next(inc = 1) {
|
||||
if (!this.active) return false
|
||||
const pending = this.pending
|
||||
if (inc !== 0) await this.pending
|
||||
if (!this.active || pending !== this.pending) return false
|
||||
const moved = await super.next(inc)
|
||||
if (inc !== 0 && moved) {
|
||||
if (!this.completion) this.selection = this.preview(this.findArgs)
|
||||
await this.selection
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
destroy() {
|
||||
return this.cancel()
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
if (!this.active) return Promise.resolve()
|
||||
++this.request
|
||||
this.active = false
|
||||
this.options = []
|
||||
this.state = "hidden"
|
||||
return this.send({ session: this.session, cancel: true }).catch(
|
||||
() => undefined,
|
||||
)
|
||||
}
|
||||
|
||||
private preview(args: string, selected = false) {
|
||||
return this.send(
|
||||
{ session: this.session, completions: false, selected },
|
||||
...args.split(/\s+/),
|
||||
)
|
||||
.then(() => undefined)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
private send(preview, ...args: string[]) {
|
||||
return this.tabId.then(tabId =>
|
||||
Messaging.messageTab(tabId, "excmd_content", "find", [
|
||||
|
|
@ -43,4 +164,8 @@ export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
|||
]),
|
||||
)
|
||||
}
|
||||
|
||||
private resize() {
|
||||
window.dispatchEvent(new Event("tridactyl-refresh-completions"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,10 @@ const NATIVE_HIGHLIGHTS = typeof Highlight === "function" && "highlights" in CSS
|
|||
|
||||
class FindHighlight extends HTMLSpanElement {
|
||||
public top = Infinity
|
||||
public nativeRange: Range
|
||||
private background = `var(--tridactyl-search-highlight-color)`
|
||||
|
||||
constructor(public range: Range) {
|
||||
super()
|
||||
this.nativeRange = NATIVE_HIGHLIGHTS ? range.cloneRange() : range
|
||||
{
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1716685
|
||||
const proto = FindHighlight.prototype
|
||||
|
|
@ -44,7 +42,9 @@ class FindHighlight extends HTMLSpanElement {
|
|||
this.style.position = "absolute"
|
||||
this.style.top = "0px"
|
||||
this.style.left = "0px"
|
||||
this.updateRectsPosition()
|
||||
const rects = this.getClientRects()
|
||||
if (!rects.length) throw new Error("Range has no rects")
|
||||
this.updateRectsPosition(rects)
|
||||
;(this as any).unfocus()
|
||||
}
|
||||
|
||||
|
|
@ -52,17 +52,15 @@ class FindHighlight extends HTMLSpanElement {
|
|||
const range = allTextNode[0].ownerDocument.createRange()
|
||||
range.setStart(allTextNode[found.startTextNodePos], found.startOffset)
|
||||
range.setEnd(allTextNode[found.endTextNodePos], found.endOffset)
|
||||
if (range.getClientRects().length < 1)
|
||||
throw new Error("Range has no rects")
|
||||
return new this(range)
|
||||
}
|
||||
|
||||
updateRectsPosition() {
|
||||
updateRectsPosition(rects = this.getClientRects()) {
|
||||
if (NATIVE_HIGHLIGHTS) {
|
||||
this.top = this.getBoundingClientRect().top + window.pageYOffset
|
||||
this.top = Array.from(rects).reduce((top, rect) =>
|
||||
rect.width || rect.height ? Math.min(top, rect.top) : top, Infinity) + window.pageYOffset
|
||||
return
|
||||
}
|
||||
const rects = this.getClientRects()
|
||||
this.top = Infinity
|
||||
const windowTop = window.pageYOffset
|
||||
const windowLeft = window.pageXOffset
|
||||
|
|
@ -99,7 +97,7 @@ class FindHighlight extends HTMLSpanElement {
|
|||
return this.range.getClientRects()
|
||||
}
|
||||
unfocus() {
|
||||
setNativeFocus(this.nativeRange, false)
|
||||
setNativeFocus(this.range, false)
|
||||
this.background = `var(--tridactyl-search-highlight-color)`
|
||||
for (const node of this.children) {
|
||||
;(node as HTMLElement).style.background = this.background
|
||||
|
|
@ -140,7 +138,7 @@ class FindHighlight extends HTMLSpanElement {
|
|||
}
|
||||
const focusable = this.queryInRange("a,input,button,details")
|
||||
if (focusElement && focusable) focusable.focus()
|
||||
setNativeFocus(this.nativeRange, true)
|
||||
setNativeFocus(this.range, true)
|
||||
this.background = `var(--tridactyl-search-highlight-active-color)`
|
||||
for (const node of this.children) {
|
||||
const element = node as HTMLElement
|
||||
|
|
@ -228,6 +226,8 @@ let lastHighlights
|
|||
let selected = 0
|
||||
let preview
|
||||
let searchGeneration = 0
|
||||
let regexSnapshots
|
||||
let regexSnapshotObservers: MutationObserver[] = []
|
||||
|
||||
let HIGHLIGHT_TIMER
|
||||
let REPOSITION_TIMER
|
||||
|
|
@ -248,14 +248,71 @@ function scheduleReposition() {
|
|||
}
|
||||
|
||||
window.addEventListener("resize", scheduleReposition)
|
||||
window.addEventListener("resize", clearRegexSnapshots)
|
||||
window.addEventListener("scroll", scheduleReposition, true)
|
||||
|
||||
function clearRegexSnapshots() {
|
||||
regexSnapshots = undefined
|
||||
regexSnapshotObservers.splice(0).forEach(observer => observer.disconnect())
|
||||
}
|
||||
|
||||
async function yieldFind(generation) {
|
||||
await new Promise(resolve => setTimeout(resolve))
|
||||
return generation !== searchGeneration
|
||||
}
|
||||
|
||||
function getRegexSnapshots(documents, cache: boolean) {
|
||||
if (cache && regexSnapshots?.length === documents.length &&
|
||||
regexSnapshots.every((snapshot, index) => snapshot.doc === documents[index])) return regexSnapshots
|
||||
if (!NATIVE_HIGHLIGHTS) getFindHost()
|
||||
const snapshots = documents.map(doc => {
|
||||
if (!doc) return { doc, nodes: [], lengths: [], text: "" }
|
||||
const painted = new WeakMap<HTMLElement, boolean>()
|
||||
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
|
||||
const nodes = []
|
||||
const lengths = []
|
||||
const parts = []
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode as Text
|
||||
const parent = node.parentElement
|
||||
if (!painted.has(parent)) painted.set(parent, DOM.isPainted(parent))
|
||||
if (painted.get(parent)) {
|
||||
nodes.push(node)
|
||||
lengths.push(node.length)
|
||||
parts.push(node.data)
|
||||
}
|
||||
}
|
||||
return { doc, nodes, lengths, text: parts.join("") }
|
||||
})
|
||||
if (cache) {
|
||||
clearRegexSnapshots()
|
||||
regexSnapshots = snapshots
|
||||
regexSnapshotObservers = documents.filter(Boolean).map(doc => {
|
||||
const observer = new MutationObserver(changes => {
|
||||
if (changes.some(({ target }) =>
|
||||
!(target as Element).closest?.("#cmdline_iframe,#TridactylFindHost")))
|
||||
clearRegexSnapshots()
|
||||
})
|
||||
observer.observe(doc, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
})
|
||||
return observer
|
||||
})
|
||||
}
|
||||
return snapshots
|
||||
}
|
||||
|
||||
export async function jumpToMatch(searchQuery, option) {
|
||||
const previewing = option["preview"] === true
|
||||
const generation = ++searchGeneration
|
||||
if (!previewing) preview = undefined
|
||||
if (previewing) clearTimeout(HIGHLIGHT_TIMER)
|
||||
else resetHighlightTimer()
|
||||
if (!previewing) {
|
||||
preview = undefined
|
||||
clearRegexSnapshots()
|
||||
}
|
||||
clearTimeout(HIGHLIGHT_TIMER)
|
||||
// First, search for the query
|
||||
const literal = option["regex"] && searchQuery.match(/^\/(.*)\/([^/]*)$/s)
|
||||
let [source, flags] = literal ? literal.slice(1) : [searchQuery, ""]
|
||||
|
|
@ -285,35 +342,40 @@ export async function jumpToMatch(searchQuery, option) {
|
|||
lastHighlights = []
|
||||
clearHighlighting()
|
||||
|
||||
const documents = [document]
|
||||
for (const frame of DOM.getAllDocumentFrames())
|
||||
if (frame.contentDocument) documents.push(frame.contentDocument)
|
||||
const nodeSets = documents.map(doc => {
|
||||
const documents = [document, ...DOM.getAllDocumentFrames().map(frame => frame.contentDocument)]
|
||||
const snapshots = regex && getRegexSnapshots(documents, previewing)
|
||||
const nodeSets = regex ? [] : documents.map(doc => {
|
||||
if (!doc) return []
|
||||
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
|
||||
const nodes = []
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode)
|
||||
return regex ? nodes.filter(n => DOM.isPainted(n.parentElement)) : nodes
|
||||
return nodes
|
||||
})
|
||||
|
||||
if (regex) {
|
||||
for (const nodes of nodeSets) {
|
||||
const found = []
|
||||
let converted = 0
|
||||
for (const { nodes, lengths, text } of snapshots) {
|
||||
let nodeIndex = 0
|
||||
let nodeOffset = 0
|
||||
const text = nodes.map(node => node.data).join("")
|
||||
for (const match of text.matchAll(regex)) {
|
||||
if (++converted % 100 === 0 && await yieldFind(generation)) return
|
||||
if (!match[0]) continue
|
||||
const end = match.index + match[0].length
|
||||
while (match.index >= nodeOffset + nodes[nodeIndex].length)
|
||||
nodeOffset += nodes[nodeIndex++].length
|
||||
const range = nodes[0].ownerDocument.createRange()
|
||||
range.setStart(nodes[nodeIndex], match.index - nodeOffset)
|
||||
while (end > nodeOffset + nodes[nodeIndex].length)
|
||||
nodeOffset += nodes[nodeIndex++].length
|
||||
range.setEnd(nodes[nodeIndex], end - nodeOffset)
|
||||
if (range.getClientRects().length)
|
||||
lastHighlights.push(new FindHighlight(range))
|
||||
try {
|
||||
const end = match.index + match[0].length
|
||||
while (match.index >= nodeOffset + lengths[nodeIndex])
|
||||
nodeOffset += lengths[nodeIndex++]
|
||||
const range = nodes[0].ownerDocument.createRange()
|
||||
range.setStart(nodes[nodeIndex], match.index - nodeOffset)
|
||||
while (end > nodeOffset + lengths[nodeIndex])
|
||||
nodeOffset += lengths[nodeIndex++]
|
||||
range.setEnd(nodes[nodeIndex], end - nodeOffset)
|
||||
if (range.toString() !== match[0]) continue
|
||||
found.push(new FindHighlight(range))
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
lastHighlights = found
|
||||
}
|
||||
for (let i = 0; i < results.count; ++i) {
|
||||
const range = results.rangeData[i]
|
||||
|
|
@ -329,6 +391,7 @@ export async function jumpToMatch(searchQuery, option) {
|
|||
throw new Error("Pattern not found: " + searchQuery)
|
||||
}
|
||||
drawHighlights(lastHighlights)
|
||||
if (!previewing) resetHighlightTimer()
|
||||
lastHighlights.sort(
|
||||
option["reverse"] ? (a, b) => b.top - a.top : (a, b) => a.top - b.top,
|
||||
)
|
||||
|
|
@ -355,7 +418,7 @@ function drawHighlights(highlights) {
|
|||
const doc = highlights[0].range.startContainer.ownerDocument
|
||||
const win: any = doc.defaultView
|
||||
const normal = new win.Highlight()
|
||||
highlights.forEach(highlight => normal.add(highlight.nativeRange))
|
||||
highlights.forEach(highlight => normal.add(highlight.range))
|
||||
const active = new win.Highlight()
|
||||
nativeRegistry = win.CSS.highlights
|
||||
normal.priority = 2147483646
|
||||
|
|
@ -381,7 +444,103 @@ function restorePreviewScrolls(snapshot = preview) {
|
|||
element.scrollTo({ left, top, behavior: "instant" })
|
||||
}
|
||||
|
||||
export async function previewMatch(session: number, searchQuery, option) {
|
||||
function truncateFindContext(text: string, length: number, before: boolean) {
|
||||
if (length < 1 || text.length <= length) return text.slice(0, length)
|
||||
if (before) {
|
||||
let start = text.length - length
|
||||
while (start > 0 && !/[\s.]/.test(text[start - 1])) --start
|
||||
return text.slice(start)
|
||||
}
|
||||
let end = length
|
||||
while (end < text.length && !/[\s.]/.test(text[end])) ++end
|
||||
if (text[end] === ".") ++end
|
||||
return text.slice(0, end)
|
||||
}
|
||||
|
||||
async function findCompletionMatches(generation) {
|
||||
const limit = config.get("findresults")
|
||||
const contextLength = Math.max(0, config.get("findcontextlen"))
|
||||
const highlights =
|
||||
limit < 0 ? lastHighlights : lastHighlights.slice(0, limit)
|
||||
const matches = []
|
||||
const headingIndexes = new Map()
|
||||
for (let index = 0; index < highlights.length; ++index) {
|
||||
const highlight = highlights[index]
|
||||
const range = highlight.range
|
||||
const doc: Document = range.startContainer.ownerDocument
|
||||
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
|
||||
walker.currentNode = range.startContainer
|
||||
let precontext = (range.startContainer as Text).data.slice(
|
||||
0,
|
||||
range.startOffset,
|
||||
)
|
||||
while (precontext.length < contextLength && walker.previousNode())
|
||||
precontext = (walker.currentNode as Text).data + precontext
|
||||
const preTruncated =
|
||||
contextLength > 0 &&
|
||||
(precontext.length > contextLength || !!walker.previousNode())
|
||||
walker.currentNode = range.endContainer
|
||||
let postcontext = (range.endContainer as Text).data.slice(
|
||||
range.endOffset,
|
||||
)
|
||||
while (postcontext.length < contextLength && walker.nextNode())
|
||||
postcontext += (walker.currentNode as Text).data
|
||||
const postTruncated =
|
||||
contextLength > 0 &&
|
||||
(postcontext.length > contextLength || !!walker.nextNode())
|
||||
precontext = truncateFindContext(precontext, contextLength, true)
|
||||
postcontext = truncateFindContext(postcontext, contextLength, false)
|
||||
let headingIndex = headingIndexes.get(doc)
|
||||
if (!headingIndex) {
|
||||
const headings = Array.from(doc.querySelectorAll("h1,h2,h3,h4,h5,h6"))
|
||||
const hierarchy = []
|
||||
const breadcrumbs = new Map()
|
||||
for (const heading of headings) {
|
||||
hierarchy.length = Number(heading.tagName[1]) - 1
|
||||
hierarchy.push(heading.textContent?.replace(/\s+/g, " ").trim())
|
||||
breadcrumbs.set(heading, hierarchy.filter(Boolean).join(" > "))
|
||||
}
|
||||
headingIndex = { headings, breadcrumbs }
|
||||
headingIndexes.set(doc, headingIndex)
|
||||
}
|
||||
const parent = (range.startContainer as Text).parentElement
|
||||
let heading = parent.closest("h1,h2,h3,h4,h5,h6")
|
||||
let start = 0
|
||||
let end = headingIndex.headings.length
|
||||
while (!heading && start < end) {
|
||||
const middle = Math.floor((start + end) / 2)
|
||||
if (
|
||||
headingIndex.headings[middle].compareDocumentPosition(
|
||||
range.startContainer,
|
||||
) === Node.DOCUMENT_POSITION_FOLLOWING
|
||||
)
|
||||
start = middle + 1
|
||||
else end = middle
|
||||
}
|
||||
heading ||= headingIndex.headings[start - 1]
|
||||
const root = doc.scrollingElement || doc.documentElement
|
||||
const top = range.getBoundingClientRect().top + doc.defaultView.scrollY
|
||||
const percent = Math.round((top / Math.max(1, root.scrollHeight)) * 100)
|
||||
matches.push({
|
||||
index,
|
||||
text: range.toString(),
|
||||
precontext: (preTruncated ? "..." : "") + precontext,
|
||||
postcontext: postcontext + (postTruncated ? "..." : ""),
|
||||
breadcrumbs: headingIndex.breadcrumbs.get(heading) || "",
|
||||
position: `${Math.min(100, Math.max(0, percent))}%`,
|
||||
})
|
||||
if (index % 100 === 99 && await yieldFind(generation)) return []
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
export async function previewMatch(
|
||||
session: number,
|
||||
searchQuery,
|
||||
option,
|
||||
completions = true,
|
||||
keepScroll = false,
|
||||
) {
|
||||
if (preview?.session !== session) {
|
||||
if (preview) cancelPreview(preview.session)
|
||||
preview = {
|
||||
|
|
@ -392,22 +551,32 @@ export async function previewMatch(session: number, searchQuery, option) {
|
|||
scrolls: new Map(),
|
||||
}
|
||||
}
|
||||
restorePreviewScrolls()
|
||||
if (keepScroll && lastHighlights?.length && "jumpTo" in option) {
|
||||
lastHighlights[selected].unfocus()
|
||||
selected = (option["jumpTo"] + lastHighlights.length) % lastHighlights.length
|
||||
return focusHighlight(selected, false)
|
||||
}
|
||||
if (!keepScroll) restorePreviewScrolls()
|
||||
const generation = searchGeneration + 1
|
||||
try {
|
||||
await jumpToMatch(searchQuery, { ...option, preview: true })
|
||||
if (preview?.session !== session || generation !== searchGeneration)
|
||||
return []
|
||||
return completions ? await findCompletionMatches(generation) : []
|
||||
} catch (_) {
|
||||
if (preview?.session === session && generation === searchGeneration) {
|
||||
clearHighlighting()
|
||||
lastHighlights = []
|
||||
restorePreviewScrolls()
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelPreview(session: number) {
|
||||
if (preview?.session !== session) return
|
||||
++searchGeneration
|
||||
clearRegexSnapshots()
|
||||
clearHighlighting()
|
||||
const snapshot = preview
|
||||
preview = undefined
|
||||
|
|
@ -502,5 +671,5 @@ export async function jumpToNextMatch(n: number, searchFromView = false) {
|
|||
}
|
||||
|
||||
export function currentMatchRange(): Range {
|
||||
return lastHighlights[selected].range
|
||||
return lastHighlights[selected].range.cloneRange()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1524,6 +1524,8 @@ export function scrollpage(n = 1, count = 1) {
|
|||
* 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.
|
||||
* `findresults`: maximum completion rows to show; `-1` is unlimited and `0` disables them.
|
||||
* `findcontextlen`: number of context characters to show around each completion.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -1567,8 +1569,9 @@ export function find(...args: string[]) {
|
|||
if (preview) {
|
||||
const { session } = preview
|
||||
if (preview.cancel) return finding.cancelPreview(session)
|
||||
const completions = preview.completions !== false && config.get("findresults") !== 0
|
||||
if (config.get("incsearch") === "true" && searchQuery.length > 0)
|
||||
return finding.previewMatch(session, searchQuery, option)
|
||||
return finding.previewMatch(session, searchQuery, option, completions, preview.selected === true)
|
||||
return finding.cancelPreview(session)
|
||||
}
|
||||
return finding.jumpToMatch(searchQuery, option)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export function getCommandlineFns(cmdline_state: {
|
|||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
cmdline_state.activeCompletions?.forEach(comp => comp.next(count))
|
||||
await Promise.all(
|
||||
cmdline_state.activeCompletions?.map(comp => comp.next(count)) || [],
|
||||
)
|
||||
},
|
||||
|
||||
/** Selects the next completion, or history line if none is selected. */
|
||||
|
|
@ -65,7 +67,9 @@ export function getCommandlineFns(cmdline_state: {
|
|||
"current_cmdline",
|
||||
"cmdline_filter",
|
||||
)
|
||||
cmdline_state.activeCompletions?.forEach(comp => comp.prev(count))
|
||||
await Promise.all(
|
||||
cmdline_state.activeCompletions?.map(comp => comp.prev(count)) || [],
|
||||
)
|
||||
},
|
||||
|
||||
/** Selects the previous completion, or history line if none is selected. */
|
||||
|
|
|
|||
|
|
@ -164,6 +164,18 @@ a.url:hover {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.FindCompletionSource .breadcrumbs {
|
||||
width: 25%;
|
||||
padding: 0 0.75em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.FindCompletionSource .position {
|
||||
width: 6em;
|
||||
padding-right: 0.75em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue