Skip to content

Commit

Permalink
Make search more usuable (#62)
Browse files Browse the repository at this point in the history
* First draft of new search using searcher api

* Fix react error claiming input went from uncontrolled to controlled

* Don't search on load unless we had a search string

* Show note name and path in search results

* Fix scrolling search results

* Fix selected background not showing in search results

* Clean up

* Clamp max scroll for sidebar in scrollable

* Cleanup

* Keep selected sidebar menu in view when scrolling via shortcuts

* Add way to scroll search results via keyboard

* Fix opening selected note from sidebar not working
  • Loading branch information
EddieAbbondanzio authored Apr 22, 2023
1 parent 491b1a1 commit eef127b
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 304 deletions.
15 changes: 8 additions & 7 deletions packages/marqus-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Section,
SerializedAppState,
} from "../shared/ui/app";
import { getNoteById, Note } from "../shared/domain/note";
import { flatten, getNoteById, Note } from "../shared/domain/note";
import { State, Listener, useStore } from "./store";
import { isTest } from "../shared/env";
import { isEmpty, tail } from "lodash";
Expand All @@ -25,8 +25,9 @@ import { Config } from "../shared/domain/config";
import { log } from "./logger";
import { arrayify } from "../shared/utils";
import { NoteDirectoryModal } from "./components/NoteDirectoryModal";
import { searchNotes } from "./components/SidebarSearch";
import { Shortcut } from "../shared/domain/shortcut";
import { MatchData, search } from "fast-fuzzy";
import { FUZZY_OPTIONS } from "./components/SidebarSearch";

async function main() {
let config: Config;
Expand Down Expand Up @@ -166,11 +167,11 @@ export async function loadInitialState(
}))
.filter(t => t.note != null) as EditorTab[];

// TODO: Find a better option than this. I suspect we need to refactor our store
// a bit to support external state better.
const searchResults = searchNotes(notes, ui.sidebar.searchString ?? "").map(
n => n.id,
);
let searchResults: MatchData<Note>[] = [];
if (ui.sidebar.searchString) {
const flatNotes = flatten(notes);
searchResults = search(ui.sidebar.searchString, flatNotes, FUZZY_OPTIONS);
}

const deserializedAppState = filterOutStaleNoteIds(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,11 @@ export const revealTabNoteInSidebar: Listener<
});
};

export function openTabsForNotes(ctx: StoreContext, noteIds: string[]): void {
export function openTabsForNotes(
ctx: StoreContext,
noteIds: string[],
newActiveTabNoteId?: string,
): void {
if (noteIds.length === 0) {
return;
}
Expand Down Expand Up @@ -740,6 +744,9 @@ export function openTabsForNotes(ctx: StoreContext, noteIds: string[]): void {
}

let { activeTabNoteId } = editor;
if (newActiveTabNoteId) {
activeTabNoteId = newActiveTabNoteId;
}
if (!activeTabNoteId) {
activeTabNoteId = tabs[0].note.id;
}
Expand Down
2 changes: 0 additions & 2 deletions packages/marqus-desktop/src/renderer/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import { Listener, Store } from "../store";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const emoji = require("remark-emoji");

const LINE_HEIGHT = 30;

type ImageAlign = "left" | "right" | "center";

export interface MarkdownProps {
Expand Down
20 changes: 7 additions & 13 deletions packages/marqus-desktop/src/renderer/components/Monaco.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Section } from "../../shared/ui/app";
import { Attachment, Protocol } from "../../shared/domain/protocols";
import { Config } from "../../shared/domain/config";
import { debounce } from "lodash";
import { useResizeObserver } from "../hooks/resizeObserver";

const DEBOUNCE_INTERVAL_MS = 250;

Expand Down Expand Up @@ -151,6 +152,12 @@ export function Monaco(props: MonacoProps): JSX.Element {
[store.state],
);

// Monaco doesn't automatically resize when it's container element does so
// we need to listen for changes and trigger the refresh ourselves.
useResizeObserver(containerElement.current, () =>
monacoEditor.current?.layout(),
);

// Mount / Unmount
useEffect(() => {
// dragenter and dragover have to be cancelled in order for the drop event
Expand All @@ -165,7 +172,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
};

const { current: el } = containerElement;
let resizeObserver: ResizeObserver | null = null;

if (el != null) {
monacoEditor.current = monaco.editor.create(el, {
Expand All @@ -179,16 +185,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
disableKeybinding(monacoEditor.current, "editor.action.triggerSuggest");
}

// Monaco doesn't automatically resize when it's container element does so
// we need to listen for changes and trigger the refresh ourselves.

resizeObserver = new ResizeObserver(() => {
if (monacoEditor.current != null) {
monacoEditor.current.layout();
}
});
resizeObserver.observe(el);

el.addEventListener("dragenter", dragEnter);
el.addEventListener("dragover", dragOver);
el.addEventListener("drop", importAttachments);
Expand All @@ -200,8 +196,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
onViewStateChange.flush();
onModelChange.flush();

resizeObserver?.disconnect();

if (monacoEditor.current != null) {
monacoEditor.current.dispose();
monacoEditor.current = null;
Expand Down
94 changes: 51 additions & 43 deletions packages/marqus-desktop/src/renderer/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { Resizable } from "./shared/Resizable";
import { Focusable } from "./shared/Focusable";
import { Store, StoreContext, Listener } from "../store";
import styled from "styled-components";
import { h100, mb3, THEME, w100 } from "../css";
import { clamp, Dictionary, head, isEmpty, keyBy, round, take } from "lodash";
import { clamp, Dictionary, head, isEmpty, keyBy, take } from "lodash";
import {
Note,
getNoteById,
Expand All @@ -26,26 +26,66 @@ import { Section } from "../../shared/ui/app";
import { deleteNoteIfConfirmed } from "../utils/deleteNoteIfConfirmed";
import { cleanupClosedTabsCache, openTabsForNotes } from "./EditorToolbar";
import { remToPx, stripUnit } from "polished";
import { Size } from "../hooks/resizeObserver";
import { incrementScroll } from "../utils/dom";

const EXPANDED_ICON = faCaretDown;
const COLLAPSED_ICON = faCaretRight;
const MIN_WIDTH = "200px";

export interface SidebarProps {
store: Store;
}

export function Sidebar(props: SidebarProps): JSX.Element {
const { store } = props;
const { state } = store;
const { input } = state.sidebar;
const { notes } = state;
const expandedLookup = keyBy(state.sidebar.expanded, e => e);
const selectedLookup = keyBy(state.sidebar.selected, s => s);
const { input, expanded, selected } = state.sidebar;

const [menus, noteIds] = useMemo(
() => renderMenus(notes, store, input, expandedLookup, selectedLookup),
[notes, store, input, expandedLookup, selectedLookup],
);
const [menus, noteIds] = useMemo(() => {
const expandedLookup = keyBy(expanded, e => e);
const selectedLookup = keyBy(selected, s => s);

return renderMenus(notes, store, input, expandedLookup, selectedLookup);
}, [notes, store, input, expanded, selected]);

const maxScroll = useRef(0);
const onSidebarHeightChange = (size: Size) => {
maxScroll.current = size.scrollHeight;
};

const scrollUp: Listener<"sidebar.scrollUp"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

return {
sidebar: {
scroll: incrementScroll(prev.sidebar.scroll, -menuHeightInt, {
max: maxScroll.current,
roundBy: menuHeightInt,
}),
},
};
});
};

const scrollDown: Listener<"sidebar.scrollDown"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

return {
sidebar: {
scroll: incrementScroll(prev.sidebar.scroll, menuHeightInt, {
max: maxScroll.current,
roundBy: menuHeightInt,
}),
},
};
});
};

useEffect(() => {
const getNext = (increment: number) => {
Expand Down Expand Up @@ -183,6 +223,7 @@ export function Sidebar(props: SidebarProps): JSX.Element {
onScroll={async s => {
await store.dispatch("sidebar.updateScroll", s);
}}
onSizeChange={onSidebarHeightChange}
>
{menus}

Expand Down Expand Up @@ -377,39 +418,6 @@ export const updateScroll: Listener<"sidebar.updateScroll"> = (
});
};

export const scrollUp: Listener<"sidebar.scrollUp"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

const newScroll = prev.sidebar.scroll - menuHeightInt;
const roundedScroll = newScroll - (newScroll % menuHeightInt);
const clampedScroll = Math.max(roundedScroll, 0);

return {
sidebar: {
scroll: clampedScroll,
},
};
});
};

export const scrollDown: Listener<"sidebar.scrollDown"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

const newScroll = prev.sidebar.scroll + menuHeightInt;
const roundedScroll = newScroll - (newScroll % menuHeightInt);

return {
sidebar: {
scroll: roundedScroll,
},
};
});
};

export const toggleNoteExpanded: Listener<
"sidebar.toggleNoteExpanded" | "sidebar.toggleSelectedNoteExpanded"
> = (ev, ctx) => {
Expand Down Expand Up @@ -747,7 +755,7 @@ export const openSelectedNotes: Listener<"sidebar.openSelectedNotes"> = async (
return;
}

openTabsForNotes(ctx, selected);
openTabsForNotes(ctx, selected, selected[0]);

// Editor is not set as focused when a note is opened from the sidebar because
// the user may not want to start editing the note yet. This makes it easier
Expand Down
38 changes: 33 additions & 5 deletions packages/marqus-desktop/src/renderer/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import styled from "styled-components";
import { PromisedInput } from "../../shared/promisedInput";
import { KeyCode, parseKeyCode } from "../../shared/io/keyCode";
import { isBlank } from "../../shared/utils";
import { Store } from "../store";
import { mt1, p2, px2, py1, THEME, w100 } from "../css";
import { px2, py1, THEME, w100 } from "../css";
import { Focusable, wasInsideFocusable } from "./shared/Focusable";
import { Icon, IconProps } from "./shared/Icon";
import { MouseDrag, useMouseDrag } from "../io/mouse";
import { Section } from "../../shared/ui/app";
import { partial } from "lodash";
import { getClosestAttribute } from "../utils/dom";
import { getClosestAttribute, isScrolledIntoView } from "../utils/dom";
import { createPortal } from "react-dom";
import { math } from "polished";

Expand Down Expand Up @@ -101,10 +107,10 @@ export function SidebarMenu(props: SidebarMenuProps): JSX.Element {
// Drag was inside sidebar, but not on a note. Move note to root.
else if (wasInsideFocusable(drag.event, Section.Sidebar)) {
onDrag();
} else {
// Drags that end outside of the sidebar should be considered cancels.
}

// Drags that end outside of the sidebar should be considered cancels.

setCursorEl(undefined);
}
},
Expand All @@ -123,6 +129,28 @@ export function SidebarMenu(props: SidebarMenuProps): JSX.Element {
}
}, [scrollToNoteId, id]);

// Scroll to menu if it's selected and offscreen
const prevIsSelectedRef = useRef(isSelected);
useLayoutEffect(() => {
const prevIsSelected = prevIsSelectedRef.current;
prevIsSelectedRef.current = isSelected;

// We only scroll to the menu when it goes from not-selected -> selected.
if (prevIsSelected || !isSelected) {
return;
}

const visCheck = isScrolledIntoView(menuRef.current);
if (visCheck.fullyVisible) {
return;
}

void store.dispatch(
"sidebar.updateScroll",
store.state.sidebar.scroll + visCheck.offBy,
);
}, [isSelected, store]);

return (
<>
<SidebarRow
Expand Down
Loading

0 comments on commit eef127b

Please sign in to comment.