Merge branch 'bookmark-folder-completions' into develop

This commit is contained in:
Vsevolod Chernetskyi 2025-02-07 13:21:13 +02:00
commit 203a90df89
5 changed files with 158 additions and 43 deletions

View file

@ -22,7 +22,10 @@ import { CompletionSourceFuse } from "@src/completions"
import { AproposCompletionSource } from "@src/completions/Apropos"
import { AutocmdCompletionSource } from "@src/completions/Autocmd"
import { BindingsCompletionSource } from "@src/completions/Bindings"
import { BmarkCompletionSource } from "@src/completions/Bmark"
import {
BmarkCompletionSource,
BookmarkFolderCompletionSource,
} from "@src/completions/Bmark"
import { CompositeCompletionSource } from "@src/completions/Composite"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
@ -70,6 +73,7 @@ const commandline_state = {
completionsDiv: window.document.getElementById("completions"),
fns: undefined as ReturnType<typeof getCommandlineFns>,
getCompletion,
getActiveCompletionSource,
history,
/** @hidden
* This is to handle Escape key which, while the cmdline is focused,
@ -100,16 +104,21 @@ function resizeArea() {
* This is a bit loosely defined at the moment.
* Should work so long as there's only one completion source per prefix.
*/
function getCompletion(args_only = false) {
function getActiveCompletionSource(): CompletionSourceFuse | undefined {
if (!commandline_state.activeCompletions) return undefined
for (const comp of commandline_state.activeCompletions) {
if (comp.state === "normal" && comp.completion !== undefined) {
return args_only ? comp.args : comp.completion
}
}
return commandline_state.activeCompletions.filter(
({ state, completion }) =>
state === "normal" && completion !== undefined,
)[0]
}
/** @hidden **/
function getCompletion(args_only = false): string | undefined {
const activeSource = getActiveCompletionSource()
if (!activeSource) return undefined
return args_only ? activeSource.args : activeSource.completion
}
commandline_state.getCompletion = getCompletion
/** @hidden **/
export function enableCompletions() {
@ -119,6 +128,7 @@ export function enableCompletions() {
// FindCompletionSource,
BindingsCompletionSource,
BmarkCompletionSource,
BookmarkFolderCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,
ExcmdCompletionSource,
@ -163,21 +173,28 @@ const noblur = () => setTimeout(() => commandline_state.clInput.focus(), 0)
/** @hidden **/
export function focus() {
function consumeBufferedPageKeys(bufferedPageKeys: string[]) {
const clInputStillFocused = window.document.activeElement === commandline_state.clInput;
logger.debug("stop_buffering_page_keys response received, bufferedPageKeys = ", bufferedPageKeys,
"clInputStillFocused = " + clInputStillFocused)
const clInputStillFocused =
window.document.activeElement === commandline_state.clInput
logger.debug(
"stop_buffering_page_keys response received, bufferedPageKeys = ",
bufferedPageKeys,
"clInputStillFocused = " + clInputStillFocused,
)
if (bufferedPageKeys.length !== 0) {
const currentClInputValue = commandline_state.clInput.value;
const initialClInputValue = commandline_state.initialClInputValue;
logger.debug("Consuming buffered page keys", bufferedPageKeys,
const currentClInputValue = commandline_state.clInput.value
const initialClInputValue = commandline_state.initialClInputValue
logger.debug(
"Consuming buffered page keys",
bufferedPageKeys,
"initialClInputValue = " + initialClInputValue,
"currentClInputValue = " + currentClInputValue);
"currentClInputValue = " + currentClInputValue,
)
// Native events are assumed to be character keydown events,
// i.e. characters appended at the end of clInput.
commandline_state.clInput.value =
initialClInputValue
+ bufferedPageKeys.join("")
+ currentClInputValue.substring(initialClInputValue.length)
initialClInputValue +
bufferedPageKeys.join("") +
currentClInputValue.substring(initialClInputValue.length)
// Update completion.
clInputValueChanged()
}
@ -185,8 +202,13 @@ export function focus() {
commandline_state.clInput.focus()
commandline_state.clInput.removeEventListener("blur", noblur)
commandline_state.clInput.addEventListener("blur", noblur)
logger.debug("commandline_frame clInput focus(), activeElement is clInput: " + (window.document.activeElement === commandline_state.clInput))
Messaging.messageOwnTab("stop_buffering_page_keys").then(consumeBufferedPageKeys)
logger.debug(
"commandline_frame clInput focus(), activeElement is clInput: " +
(window.document.activeElement === commandline_state.clInput),
)
Messaging.messageOwnTab("stop_buffering_page_keys").then(
consumeBufferedPageKeys,
)
}
/** @hidden **/
@ -210,7 +232,10 @@ commandline_state.clInput.addEventListener(
"keydown",
function (keyevent: KeyboardEvent) {
if (!keyevent.isTrusted) return
logger.debug("commandline_frame clInput keydown event listener", keyevent)
logger.debug(
"commandline_frame clInput keydown event listener",
keyevent,
)
commandline_state.keyEvents.push(minimalKeyFromKeyboardEvent(keyevent))
const response = keyParser(commandline_state.keyEvents)
if (response.isMatch) {
@ -288,7 +313,7 @@ let onInputPromise: Promise<any> = Promise.resolve()
/** @hidden **/
commandline_state.clInput.addEventListener("input", () => {
logger.debug("commandline_frame clInput input event listener")
clInputValueChanged();
clInputValueChanged()
})
/** @hidden **/
@ -370,7 +395,15 @@ export function fillcmdline(
trailspace = true,
ffocus = true,
) {
logger.debug("commandline_frame fillcmdline(newcommand = " + newcommand + " trailspace = " + trailspace + " ffocus = " + ffocus + ")")
logger.debug(
"commandline_frame fillcmdline(newcommand = " +
newcommand +
" trailspace = " +
trailspace +
" ffocus = " +
ffocus +
")",
)
if (trailspace) commandline_state.clInput.value = newcommand + " "
else commandline_state.clInput.value = newcommand
commandline_state.initialClInputValue = commandline_state.clInput.value

View file

@ -37,12 +37,13 @@ export abstract class CompletionSource {
node: HTMLElement
public completion: string
public args: string
public trailingSpace: boolean
protected prefixes: string[] = []
protected lastFocused: CompletionOption
private _state: OptionState
private _prevState: OptionState
constructor(prefixes) {
constructor(prefixes, options = { trailingSpace: true }) {
const commands = aliases.getCmdAliasMapping()
// Now, for each prefix given as argument, add it to the completionsource's prefix list and also add any alias it has
@ -56,6 +57,7 @@ export abstract class CompletionSource {
// Not sure this is necessary but every completion source has it
this.prefixes = this.prefixes.map(p => p + " ")
this.trailingSpace = options.trailingSpace
}
/** Control presentation of Source */
@ -174,8 +176,13 @@ export abstract class CompletionSourceFuse extends CompletionSource {
protected optionContainer = html`<table class="optionContainer"></table>`
constructor(prefixes, className: string, title?: string) {
super(prefixes)
constructor(
prefixes,
className: string,
title?: string,
options = { trailingSpace: true },
) {
super(prefixes, options)
this.node = html`<div class="${className} hidden">
<div class="sectionHeader">${title || className}</div>
</div>`

View file

@ -107,3 +107,56 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
}
}
}
export class BookmarkFolderCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent) {
super(["bmark"], "BookmarkFolderCompletionSource", "Bookmark Folders", {
trailingSpace: false,
})
}
async onInput(exstr: string) {
const [_command, _url, path] = this.parseArgs(exstr)
if (path == undefined) {
this.options = undefined
return
}
this.options = (await providers.getBookmarkFolders(path))
.slice(0, 10)
.map(path => new BookmarkFolderCompletionOption(path))
}
splitOnPrefix(exstr: string): string[] {
const [command, url, path] = this.parseArgs(exstr)
return [`${command} ${url}`, path]
}
private parseArgs(exstr: string): string[] {
const [command, args] = super.splitOnPrefix(exstr)
if (!args) {
return [command]
}
const spaceIndex = args.search(/\s+/)
const url = args.slice(0, spaceIndex)
if (spaceIndex == -1) {
return [command, url]
}
const path = args.slice(spaceIndex + 1)
return [command, url, path]
}
}
class BookmarkFolderCompletionOption
extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
fuseKeys = []
constructor(public value: string) {
super()
this.fuseKeys.push(value)
this.html = html`<tr class="BookmarkFolderCompletionOption option">
<td class="prefix">${"".padEnd(2)}</td>
<td class="title">${value}</td>
</tr>`
}
}

View file

@ -66,17 +66,7 @@ async function fuseBookmarksSearch(query: string): Promise<Bookmark[]> {
}
async function collectBookmarks(): Promise<Bookmark[]> {
const root = await browserBg.bookmarks.getTree()
const bookmarks = root.flatMap(flattenChildren)
const bookmarksDictionary = bookmarks.reduce((dict, bookmark) => {
dict[bookmark.id] = bookmark
return dict
}, {})
return bookmarks
.map(bookmark => ({
path: buildBookmarkPath("", bookmark, bookmarksDictionary),
...bookmark,
}))
return (await collectBookmarksAndFolders())
.filter(isValidBookmark)
.sort((a, b) => b.dateAdded - a.dateAdded)
}
@ -117,6 +107,34 @@ function isValidBookmark(bookmark: Bookmark): boolean {
}
}
let bookmarkPaths: string[]
export async function getBookmarkFolders(query: string) {
bookmarkPaths = bookmarkPaths || [
...new Set(
(await collectBookmarksAndFolders())
.filter(bookmark => bookmark.path && bookmark.path != "/")
.map(bookmark => bookmark.path),
),
]
return query
? bookmarkPaths.filter(path => path != query && path.includes(query))
: bookmarkPaths
}
async function collectBookmarksAndFolders(): Promise<Bookmark[]> {
const root = await browserBg.bookmarks.getTree()
const bookmarks = root.flatMap(flattenChildren)
const bookmarksDictionary = bookmarks.reduce((dict, bookmark) => {
dict[bookmark.id] = bookmark
return dict
}, {})
return bookmarks.map(bookmark => ({
path: buildBookmarkPath("", bookmark, bookmarksDictionary),
...bookmark,
}))
}
export async function getSearchUrls(query: string) {
const suconf = config.get("searchurls")

View file

@ -78,15 +78,17 @@ export function getCommandlineFns(cmdline_state: {
"current_cmdline",
"cmdline_filter",
)
const command = cmdline_state.getCompletion()
const completionSource = cmdline_state.getActiveCompletionSource()
const completion = completionSource?.completion
if (cmdline_state.activeCompletions) {
cmdline_state.activeCompletions.forEach(
comp => (comp.completion = undefined),
)
}
let result = Promise.resolve([])
if (command) {
cmdline_state.clInput.value = command + " "
if (completion) {
cmdline_state.clInput.value =
completion + (completionSource?.trailingSpace ? " " : "")
result = cmdline_state.refresh_completions(
cmdline_state.clInput.value,
)
@ -99,14 +101,16 @@ export function getCommandlineFns(cmdline_state: {
* If no completion is selected, inserts a space where the caret is.
*/
insert_space_or_completion: () => {
const command = cmdline_state.getCompletion()
const completionSource = cmdline_state.getActiveCompletionSource()
const completion = completionSource?.completion
if (cmdline_state.activeCompletions) {
cmdline_state.activeCompletions.forEach(
comp => (comp.completion = undefined),
)
}
if (command) {
cmdline_state.clInput.value = command + " "
if (completion) {
cmdline_state.clInput.value =
completion + (completionSource?.trailingSpace ? " " : "")
} else {
space(cmdline_state)
}