Initial search implementation
This commit is contained in:
@@ -6,9 +6,11 @@ import { FilterOption } from "@silverbulletmd/common/types";
|
||||
|
||||
export function CommandPalette({
|
||||
commands,
|
||||
recentCommands,
|
||||
onTrigger,
|
||||
}: {
|
||||
commands: Map<string, AppCommand>;
|
||||
recentCommands: Map<string, Date>;
|
||||
onTrigger: (command: AppCommand | undefined) => void;
|
||||
}) {
|
||||
let options: FilterOption[] = [];
|
||||
@@ -17,6 +19,9 @@ export function CommandPalette({
|
||||
options.push({
|
||||
name: name,
|
||||
hint: isMac && def.command.mac ? def.command.mac : def.command.key,
|
||||
orderId: recentCommands.has(name)
|
||||
? -recentCommands.get(name)!.getTime()
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -2,49 +2,24 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FilterOption } from "@silverbulletmd/common/types";
|
||||
import fuzzysort from "fuzzysort";
|
||||
|
||||
function magicSorter(a: FilterOption, b: FilterOption): number {
|
||||
if (a.orderId && b.orderId) {
|
||||
return a.orderId < b.orderId ? -1 : 1;
|
||||
}
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
if (a.orderId) {
|
||||
return -1;
|
||||
}
|
||||
if (b.orderId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function escapeRegExp(str: string): string {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
|
||||
function fuzzyFilter(pattern: string, options: FilterOption[]): FilterOption[] {
|
||||
let closeMatchRegex = escapeRegExp(pattern);
|
||||
closeMatchRegex = closeMatchRegex.split(/\s+/).join(".*?");
|
||||
closeMatchRegex = closeMatchRegex.replace(/\\\//g, ".*?\\/.*?");
|
||||
const distantMatchRegex = escapeRegExp(pattern).split("").join(".*?");
|
||||
const r1 = new RegExp(closeMatchRegex, "i");
|
||||
const r2 = new RegExp(distantMatchRegex, "i");
|
||||
let matches = [];
|
||||
if (!pattern) {
|
||||
return options;
|
||||
}
|
||||
for (let option of options) {
|
||||
let m = r1.exec(option.name);
|
||||
if (m) {
|
||||
matches.push({
|
||||
...option,
|
||||
orderId: 100000 - (options.length - m[0].length - m.index),
|
||||
});
|
||||
} else {
|
||||
// Let's try the distant matcher
|
||||
var m2 = r2.exec(option.name);
|
||||
if (m2) {
|
||||
matches.push({
|
||||
...option,
|
||||
orderId: 10000 - (options.length - m2[0].length - m2.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
type FilterResult = FilterOption & {
|
||||
result?: any;
|
||||
};
|
||||
|
||||
function simpleFilter(
|
||||
pattern: string,
|
||||
@@ -56,6 +31,25 @@ function simpleFilter(
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function fuzzySorter(pattern: string, options: FilterOption[]): FilterResult[] {
|
||||
return fuzzysort
|
||||
.go(pattern, options, {
|
||||
all: true,
|
||||
key: "name",
|
||||
})
|
||||
.map((result) => ({ ...result.obj, result: result }))
|
||||
.sort(magicSorter);
|
||||
}
|
||||
|
||||
export function FilterList({
|
||||
placeholder,
|
||||
options,
|
||||
@@ -82,7 +76,7 @@ export function FilterList({
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
const [text, setText] = useState("");
|
||||
const [matchingOptions, setMatchingOptions] = useState(
|
||||
options.sort(magicSorter)
|
||||
fuzzySorter("", options)
|
||||
);
|
||||
const [selectedOption, setSelectionOption] = useState(0);
|
||||
|
||||
@@ -93,23 +87,15 @@ export function FilterList({
|
||||
}
|
||||
|
||||
function updateFilter(originalPhrase: string) {
|
||||
const searchPhrase = originalPhrase.toLowerCase();
|
||||
|
||||
if (searchPhrase) {
|
||||
let foundExactMatch = false;
|
||||
let results = simpleFilter(searchPhrase, options);
|
||||
results = results.sort(magicSorter);
|
||||
if (allowNew && !foundExactMatch) {
|
||||
results.push({
|
||||
name: originalPhrase,
|
||||
hint: newHint,
|
||||
});
|
||||
}
|
||||
setMatchingOptions(results);
|
||||
} else {
|
||||
let results = options.sort(magicSorter);
|
||||
setMatchingOptions(results);
|
||||
let foundExactMatch = false;
|
||||
let results = fuzzySorter(originalPhrase, options);
|
||||
if (allowNew && !foundExactMatch) {
|
||||
results.push({
|
||||
name: originalPhrase,
|
||||
hint: newHint,
|
||||
});
|
||||
}
|
||||
setMatchingOptions(results);
|
||||
|
||||
setText(originalPhrase);
|
||||
setSelectionOption(0);
|
||||
@@ -201,7 +187,14 @@ export function FilterList({
|
||||
<span className="icon">
|
||||
{icon && <FontAwesomeIcon icon={icon} />}
|
||||
</span>
|
||||
<span className="name">{option.name}</span>
|
||||
<span
|
||||
className="name"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: option?.result?.indexes
|
||||
? fuzzysort.highlight(option.result, "<b>", "</b>")!
|
||||
: escapeHtml(option.name),
|
||||
}}
|
||||
></span>
|
||||
{option.hint && <span className="hint">{option.hint}</span>}
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -602,12 +602,14 @@ export class Editor {
|
||||
dispatch({ type: "hide-palette" });
|
||||
editor!.focus();
|
||||
if (cmd) {
|
||||
dispatch({ type: "command-run", command: cmd.command.name });
|
||||
cmd.run().catch((e) => {
|
||||
console.error("Error running command", e.message);
|
||||
});
|
||||
}
|
||||
}}
|
||||
commands={viewState.commands}
|
||||
recentCommands={viewState.recentCommands}
|
||||
/>
|
||||
)}
|
||||
{viewState.showFilterBox && (
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { createImportSpecifier } from "typescript";
|
||||
|
||||
const urlRegexp =
|
||||
/^https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
||||
|
||||
// Known iOS Safari paste issue (unrelated to this implementation): https://voxpelli.com/2015/03/ios-safari-url-copy-paste-bug/
|
||||
export const pasteLinkExtension = ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate): void {
|
||||
@@ -19,6 +21,7 @@ export const pasteLinkExtension = ViewPlugin.fromClass(
|
||||
let pastedString = pastedText.join("");
|
||||
if (pastedString.match(urlRegexp)) {
|
||||
let selection = update.startState.selection.main;
|
||||
console.log("It's a URL and selection empty?", selection.empty);
|
||||
if (!selection.empty) {
|
||||
setTimeout(() => {
|
||||
update.view.dispatch({
|
||||
|
||||
Generated
+14606
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,7 @@
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@lezer/markdown": "^0.15.0",
|
||||
"fake-indexeddb": "^3.1.7",
|
||||
"fuzzysort": "^1.9.0",
|
||||
"jest": "^27.5.1",
|
||||
"knex": "^1.0.4",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -66,6 +66,11 @@ export default function reducer(
|
||||
...state,
|
||||
showCommandPalette: false,
|
||||
};
|
||||
case "command-run":
|
||||
return {
|
||||
...state,
|
||||
recentCommands: state.recentCommands.set(action.command, new Date()),
|
||||
};
|
||||
case "update-commands":
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppCommand } from "./hooks/command";
|
||||
import { AppCommand, CommandDef } from "./hooks/command";
|
||||
import { FilterOption, PageMeta } from "@silverbulletmd/common/types";
|
||||
|
||||
export const slashCommandRegexp = /\/[\w\-]*/;
|
||||
@@ -34,6 +34,7 @@ export type AppViewState = {
|
||||
commands: Map<string, AppCommand>;
|
||||
notifications: Notification[];
|
||||
actionButtons: ActionButton[];
|
||||
recentCommands: Map<string, Date>;
|
||||
|
||||
showFilterBox: boolean;
|
||||
filterBoxLabel: string;
|
||||
@@ -55,6 +56,7 @@ export const initialViewState: AppViewState = {
|
||||
bhsHTML: "",
|
||||
allPages: new Set(),
|
||||
commands: new Map(),
|
||||
recentCommands: new Map(),
|
||||
notifications: [],
|
||||
actionButtons: [],
|
||||
showFilterBox: false,
|
||||
@@ -87,6 +89,7 @@ export type Action =
|
||||
| { type: "hide-lhs" }
|
||||
| { type: "show-bhs"; html: string; flex: number; script?: string }
|
||||
| { type: "hide-bhs" }
|
||||
| { type: "command-run"; command: string }
|
||||
| {
|
||||
type: "show-filterbox";
|
||||
options: FilterOption[];
|
||||
|
||||
Reference in New Issue
Block a user