From 57c7454e01af7773da4c5a6dd8fc0a3d71cc1240 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 15 May 2025 21:05:09 -0400 Subject: [PATCH 1/2] curr progress --- .../roam/src/components/DiscourseNodeMenu.tsx | 8 +- .../components/DiscourseNodeSearchMenu.tsx | 126 ++++++++++++++++-- .../settings/HomePersonalSettings.tsx | 12 ++ .../utils/initializeObserversAndListeners.ts | 104 +++++++++++---- 4 files changed, 214 insertions(+), 36 deletions(-) diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index 53375fee3..13455b1c6 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -197,18 +197,18 @@ export const render = (props: Props) => { }; // node_modules\@blueprintjs\core\lib\esm\components\hotkeys\hotkeyParser.js -const isMac = () => { +export const isMac = () => { const platform = typeof navigator !== "undefined" ? navigator.platform : undefined; return platform == null ? false : /Mac|iPod|iPhone|iPad/.test(platform); }; -const MODIFIER_BIT_MASKS = { +export const MODIFIER_BIT_MASKS = { alt: 1, ctrl: 2, meta: 4, shift: 8, }; -const ALIASES: { [key: string]: string } = { +export const ALIASES: { [key: string]: string } = { cmd: "meta", command: "meta", escape: "esc", @@ -219,7 +219,7 @@ const ALIASES: { [key: string]: string } = { return: "enter", win: "meta", }; -const normalizeKeyCombo = (combo: string) => { +export const normalizeKeyCombo = (combo: string) => { const keys = combo.replace(/\s/g, "").split("+"); return keys.map(function (key) { const keyName = ALIASES[key] != null ? ALIASES[key] : key; diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index b5acc8ab0..3c3192675 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -12,6 +12,9 @@ import { Position, Checkbox, Button, + InputGroup, + IKeyCombo, + getKeyCombo, } from "@blueprintjs/core"; import ReactDOM from "react-dom"; import getUids from "roamjs-components/dom/getUids"; @@ -23,11 +26,14 @@ import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import { escapeCljString } from "~/utils/formatUtils"; import { Result } from "~/utils/types"; +import { OnloadArgs } from "roamjs-components/types"; +import { getModifiersFromCombo, normalizeKeyCombo } from "./DiscourseNodeMenu"; type Props = { textarea: HTMLTextAreaElement; triggerPosition: number; onClose: () => void; + extensionAPI: OnloadArgs["extensionAPI"]; }; const waitForBlock = ( @@ -51,6 +57,7 @@ const NodeSearchMenu = ({ onClose, textarea, triggerPosition, + extensionAPI, }: { onClose: () => void } & Props) => { const [activeIndex, setActiveIndex] = useState(0); const [searchTerm, setSearchTerm] = useState(""); @@ -236,19 +243,15 @@ const NodeSearchMenu = ({ ); const handleTextAreaInput = useCallback(() => { - const atTriggerRegex = /@(.*)$/; - const textBeforeCursor = textarea.value.substring( + const textAfterTrigger = textarea.value.substring( triggerPosition, textarea.selectionStart, ); - const match = atTriggerRegex.exec(textBeforeCursor); - if (match) { - debouncedSearchTerm(match[1]); - } else { - onClose(); - return; + + if (textAfterTrigger.length > 0) { + debouncedSearchTerm(textAfterTrigger); } - }, [textarea, onClose, debouncedSearchTerm, triggerPosition]); + }, [textarea, debouncedSearchTerm, triggerPosition]); const keydownListener = useCallback( (e: KeyboardEvent) => { @@ -520,4 +523,109 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => { ); }; +export const NodeSearchMenuTriggerComponent = ({ + extensionAPI, +}: { + extensionAPI: OnloadArgs["extensionAPI"]; +}) => { + const inputRef = useRef(null); + const [isActive, setIsActive] = useState(false); + const [comboKey, setComboKey] = useState( + () => + (extensionAPI.settings.get( + "discourse-node-search-trigger", + ) as IKeyCombo) || { modifiers: 0, key: "@" }, + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + const comboObj = getKeyCombo(e.nativeEvent); + if (!comboObj.key) return; + const specialCharMap = { + // Numbers row + "~": { key: "`", modifiers: 8 }, + "!": { key: "1", modifiers: 8 }, + "@": { key: "2", modifiers: 8 }, + "#": { key: "3", modifiers: 8 }, + $: { key: "4", modifiers: 8 }, + "%": { key: "5", modifiers: 8 }, + "^": { key: "6", modifiers: 8 }, + "&": { key: "7", modifiers: 8 }, + "*": { key: "8", modifiers: 8 }, + "(": { key: "9", modifiers: 8 }, + ")": { key: "0", modifiers: 8 }, + _: { key: "-", modifiers: 8 }, + "+": { key: "=", modifiers: 8 }, + + // Brackets and punctuation + "{": { key: "[", modifiers: 8 }, + "}": { key: "]", modifiers: 8 }, + "|": { key: "\\", modifiers: 8 }, + ":": { key: ";", modifiers: 8 }, + '"': { key: "'", modifiers: 8 }, + "<": { key: ",", modifiers: 8 }, + ">": { key: ".", modifiers: 8 }, + "?": { key: "/", modifiers: 8 }, + }; + + for (const [specialChar, mapping] of Object.entries(specialCharMap)) { + if ( + comboObj.key === mapping.key && + comboObj.modifiers === mapping.modifiers + ) { + setComboKey({ modifiers: 0, key: specialChar }); + extensionAPI.settings.set("discourse-node-search-trigger", { + modifiers: 0, + key: specialChar, + }); + return; + } + } + + setComboKey(comboObj); + extensionAPI.settings.set("discourse-node-search-trigger", comboObj); + }, + [extensionAPI], + ); + + const shortcut = useMemo(() => { + if (!comboKey.key) return ""; + + const modifiers = getModifiersFromCombo(comboKey); + const comboString = [...modifiers, comboKey.key].join("+"); + return normalizeKeyCombo(comboString).join("+"); + }, [comboKey]); + + return ( + setIsActive(true)} + onBlur={() => setIsActive(false)} + rightElement={ +