SilverBullet pivot to become an offline-first PWA (#403)
This commit is contained in:
@@ -2,7 +2,7 @@ import { isMacLike } from "../../common/util.ts";
|
||||
import { FilterList } from "./filter.tsx";
|
||||
import { CompletionContext, CompletionResult, TerminalIcon } from "../deps.ts";
|
||||
import { AppCommand } from "../hooks/command.ts";
|
||||
import { FilterOption } from "../../common/types.ts";
|
||||
import { FilterOption } from "../types.ts";
|
||||
|
||||
export function CommandPalette({
|
||||
commands,
|
||||
|
||||
+18
-60
@@ -5,58 +5,16 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "../deps.ts";
|
||||
import { FilterOption } from "../../common/types.ts";
|
||||
import fuzzysort from "https://esm.sh/fuzzysort@2.0.1";
|
||||
import { FilterOption } from "../types.ts";
|
||||
import { FunctionalComponent } from "https://esm.sh/v99/preact@10.11.3/src/index";
|
||||
import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types";
|
||||
import { MiniEditor } from "./mini_editor.tsx";
|
||||
|
||||
function magicSorter(a: FilterOption, b: FilterOption): number {
|
||||
if (a.orderId && b.orderId) {
|
||||
return a.orderId < b.orderId ? -1 : 1;
|
||||
}
|
||||
if (a.orderId) {
|
||||
return -1;
|
||||
}
|
||||
if (b.orderId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
import { fuzzySearchAndSort } from "./fuzzy_search.ts";
|
||||
|
||||
type FilterResult = FilterOption & {
|
||||
result?: any;
|
||||
};
|
||||
|
||||
function simpleFilter(
|
||||
pattern: string,
|
||||
options: FilterOption[],
|
||||
): FilterOption[] {
|
||||
const lowerPattern = pattern.toLowerCase();
|
||||
return options.filter((option) => {
|
||||
return option.name.toLowerCase().includes(lowerPattern);
|
||||
});
|
||||
}
|
||||
|
||||
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: any) => ({ ...result.obj, result: result }))
|
||||
.sort(magicSorter);
|
||||
}
|
||||
|
||||
export function FilterList({
|
||||
placeholder,
|
||||
options,
|
||||
@@ -88,30 +46,31 @@ export function FilterList({
|
||||
}) {
|
||||
const [text, setText] = useState("");
|
||||
const [matchingOptions, setMatchingOptions] = useState(
|
||||
fuzzySorter("", options),
|
||||
fuzzySearchAndSort(options, ""),
|
||||
);
|
||||
const [selectedOption, setSelectionOption] = useState(0);
|
||||
|
||||
const selectedElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function updateFilter(originalPhrase: string) {
|
||||
const foundExactMatch = false;
|
||||
const results = fuzzySorter(originalPhrase, options);
|
||||
const results = fuzzySearchAndSort(options, originalPhrase);
|
||||
const foundExactMatch = !!results.find((result) =>
|
||||
result.name === originalPhrase
|
||||
);
|
||||
if (allowNew && !foundExactMatch && originalPhrase) {
|
||||
results.splice(1, 0, {
|
||||
name: originalPhrase,
|
||||
hint: newHint,
|
||||
});
|
||||
}
|
||||
setMatchingOptions(results);
|
||||
|
||||
// setText(originalPhrase);
|
||||
setMatchingOptions(results);
|
||||
setSelectionOption(0);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter(text);
|
||||
}, [options]);
|
||||
}, [options, text]);
|
||||
|
||||
useEffect(() => {
|
||||
function closer() {
|
||||
@@ -147,7 +106,8 @@ export function FilterList({
|
||||
onSelect(undefined);
|
||||
}}
|
||||
onChange={(text) => {
|
||||
updateFilter(text);
|
||||
setText(text);
|
||||
// updateFilter(text);
|
||||
}}
|
||||
onKeyUp={(view, e) => {
|
||||
// This event is triggered after the key has been processed by CM already
|
||||
@@ -182,7 +142,7 @@ export function FilterList({
|
||||
const text = view.state.sliceDoc();
|
||||
if (completePrefix && text === "") {
|
||||
setText(completePrefix);
|
||||
updateFilter(completePrefix);
|
||||
// updateFilter(completePrefix);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@@ -210,7 +170,6 @@ export function FilterList({
|
||||
setSelectionOption(idx);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
console.log("Selecting", option);
|
||||
e.stopPropagation();
|
||||
onSelect(option);
|
||||
}}
|
||||
@@ -220,14 +179,13 @@ export function FilterList({
|
||||
<Icon width={16} height={16} />
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="sb-name"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: option?.result?.indexes
|
||||
? fuzzysort.highlight(option.result, "<b>", "</b>")!
|
||||
: escapeHtml(option.name),
|
||||
}}
|
||||
<span className="sb-name" // dangerouslySetInnerHTML={{
|
||||
// __html: option?.result?.indexes
|
||||
// ? fuzzysort.highlight(option.result, "<b>", "</b>")!
|
||||
// : escapeHtml(option.name),
|
||||
// }}
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
{option.hint && <span className="sb-hint">{option.hint}</span>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { FilterOption } from "../types.ts";
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import { fuzzySearchAndSort } from "./fuzzy_search.ts";
|
||||
|
||||
Deno.test("testFuzzyFilter", () => {
|
||||
const array: FilterOption[] = [
|
||||
{ name: "My Company/Hank", orderId: -5 },
|
||||
{ name: "My Company/Steve Co", orderId: -5 },
|
||||
{ name: "Other/Steve", orderId: -7 },
|
||||
{ name: "Steve", orderId: -3 },
|
||||
];
|
||||
|
||||
// Prioritize match in last path part
|
||||
const result = fuzzySearchAndSort(array, "Co");
|
||||
assertEquals(result.length, 2);
|
||||
assertEquals(result[0].name, "My Company/Steve Co");
|
||||
|
||||
// Support slash matches
|
||||
const result2 = fuzzySearchAndSort(array, "Co/St");
|
||||
assertEquals(result2.length, 1);
|
||||
assertEquals(result2[0].name, "My Company/Steve Co");
|
||||
|
||||
// Find "St" in both, but pioritize based on orderId
|
||||
const result3 = fuzzySearchAndSort(array, "St");
|
||||
assertEquals(result3.length, 3);
|
||||
assertEquals(result3[0].name, "Other/Steve");
|
||||
|
||||
const result4 = fuzzySearchAndSort(array, "Steve");
|
||||
assertEquals(result4[0].name, "Steve");
|
||||
|
||||
// const result2 = fuzzySearchAndSort(array, "");
|
||||
// console.log("Result 2", result2);
|
||||
// assertEquals(result.length, 3);
|
||||
// assertEquals(result[0].orderId, 1);
|
||||
|
||||
// assertEquals(result[1].name, "Jack");
|
||||
// assertEquals(result[1].orderId, 2);
|
||||
// assertEquals(result[2].name, "Jill");
|
||||
// assertEquals(result[2].orderId, 3);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FilterOption } from "../types.ts";
|
||||
|
||||
export const fuzzySearchAndSort = (
|
||||
arr: FilterOption[],
|
||||
searchPhrase: string,
|
||||
): FilterOption[] => {
|
||||
// Prepare regular expression: escape special characters, add '.*' around each character
|
||||
const safePhrase = searchPhrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // escape special characters
|
||||
const searchRegex = new RegExp(Array.from(safePhrase).join(".*"), "i"); // 'i' makes it case-insensitive
|
||||
|
||||
// Fuzzy matching on name using the regular expression
|
||||
const filtered = arr.filter((item) => searchRegex.test(item.name));
|
||||
|
||||
// Sorting by exact match, whether match is in part after '/', then by orderId
|
||||
filtered.sort((a, b) => {
|
||||
const aNamePart = a.name.includes("/")
|
||||
? a.name.split("/").pop() || ""
|
||||
: a.name;
|
||||
const bNamePart = b.name.includes("/")
|
||||
? b.name.split("/").pop() || ""
|
||||
: b.name;
|
||||
|
||||
const aMatchInPart = searchRegex.test(aNamePart);
|
||||
const bMatchInPart = searchRegex.test(bNamePart);
|
||||
|
||||
// Check for exact match
|
||||
const aExactMatch = a.name.toLowerCase() === searchPhrase.toLowerCase();
|
||||
const bExactMatch = b.name.toLowerCase() === searchPhrase.toLowerCase();
|
||||
|
||||
if (aExactMatch !== bExactMatch) {
|
||||
// If one is an exact match and the other is not, prioritize the exact match
|
||||
return aExactMatch ? -1 : 1;
|
||||
} else if (aMatchInPart !== bMatchInPart) {
|
||||
// If one matches in the part after '/' and the other doesn't, prioritize the one that does
|
||||
return aMatchInPart ? -1 : 1;
|
||||
} else {
|
||||
// If both match in the same part of name, prioritize by orderId
|
||||
const aOrder = a.orderId !== undefined ? a.orderId : Infinity;
|
||||
const bOrder = b.orderId !== undefined ? b.orderId : Infinity;
|
||||
return aOrder - bOrder;
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
@@ -253,6 +253,12 @@ export function MiniEditor(
|
||||
// Reset the state
|
||||
view.setState(buildEditorState());
|
||||
});
|
||||
} else if (focus) {
|
||||
// console.log("BLURRING WHILE KEEPING FOCUSE");
|
||||
// Automatically refocus blurred
|
||||
if (editorViewRef.current) {
|
||||
editorViewRef.current.focus();
|
||||
}
|
||||
}
|
||||
// Event may occur again in 500ms
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FilterList } from "./filter.tsx";
|
||||
import { FilterOption, PageMeta } from "../../common/types.ts";
|
||||
import { FilterOption, PageMeta } from "../types.ts";
|
||||
import { CompletionContext, CompletionResult } from "../deps.ts";
|
||||
|
||||
export function PageNavigator({
|
||||
@@ -10,7 +10,7 @@ export function PageNavigator({
|
||||
darkMode,
|
||||
currentPage,
|
||||
}: {
|
||||
allPages: Set<PageMeta>;
|
||||
allPages: PageMeta[];
|
||||
vimMode: boolean;
|
||||
darkMode: boolean;
|
||||
onNavigate: (page: string | undefined) => void;
|
||||
|
||||
@@ -18,6 +18,7 @@ export type ActionButton = {
|
||||
export function TopBar({
|
||||
pageName,
|
||||
unsavedChanges,
|
||||
synced,
|
||||
isLoading,
|
||||
notifications,
|
||||
onRename,
|
||||
@@ -30,6 +31,7 @@ export function TopBar({
|
||||
}: {
|
||||
pageName?: string;
|
||||
unsavedChanges: boolean;
|
||||
synced: boolean;
|
||||
isLoading: boolean;
|
||||
notifications: Notification[];
|
||||
darkMode: boolean;
|
||||
@@ -73,7 +75,7 @@ export function TopBar({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="sb-top">
|
||||
<div id="sb-top" className={synced ? undefined : "sb-sync-error"}>
|
||||
{lhs}
|
||||
<div className="main">
|
||||
<div className="inner">
|
||||
|
||||
Reference in New Issue
Block a user