From 18cabe00a6acd81d590dcfcb74d0e48efb33a4e5 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 10 May 2024 17:11:18 -0500 Subject: [PATCH 01/12] src/components/FileItem: Added stopPropogation to context menu to prevent bubbling up from parent Signed-off-by: MoTheRoar --- src/components/File/FileSideBar/FileItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/File/FileSideBar/FileItem.tsx b/src/components/File/FileSideBar/FileItem.tsx index 7024715c..3f284925 100644 --- a/src/components/File/FileSideBar/FileItem.tsx +++ b/src/components/File/FileSideBar/FileItem.tsx @@ -73,6 +73,7 @@ export const FileItem: React.FC = ({ const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); + e.stopPropagation(); // Prevent event from bubbling up to the parent. window.contextMenu.showFileItemContextMenu(file); }; From 75cad740d75c8e81b80f8d4f1d6a550685965f5d Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 10 May 2024 17:12:30 -0500 Subject: [PATCH 02/12] electron/main/index: Added new ipcMain handler for dropdown menu on rightclick Signed-off-by: MoTheRoar --- electron/main/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/electron/main/index.ts b/electron/main/index.ts index e9fed329..8785bc59 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -216,6 +216,32 @@ ipcMain.handle("show-context-menu-file-item", (event, file) => { } }); +ipcMain.handle("show-context-menu-item", (event) => { + const menu = new Menu(); + + menu.append( + new MenuItem({ + label: "New Note", + click: () => { + event.sender.send("add-new-note-listener"); + }, + }) + ); + + menu.append( + new MenuItem({ + label: "New Directory", + click: () => { + event.sender.send("add-new-directory-listener"); + }, + }) + ); + + const browserWindow = BrowserWindow.fromWebContents(event.sender); + if (browserWindow) + menu.popup({ window: browserWindow }) +}); + ipcMain.handle("open-external", (event, url) => { shell.openExternal(url); }); From 9c48b695e0b62cb0c97f61184042624511f4c8a4 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 10 May 2024 17:13:22 -0500 Subject: [PATCH 03/12] electron/preload/index: Added new contextMenu to invoke our ipcMain handler Signed-off-by: MoTheRoar --- electron/preload/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index d6fc089c..3b77c021 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -36,6 +36,9 @@ declare global { contextMenu: { showFileItemContextMenu: (filePath: FileInfoNode) => void; }; + contextFileMenu: { + showMenuItemContext: () => void; + } database: { search: ( query: string, @@ -306,6 +309,12 @@ contextBridge.exposeInMainWorld("contextMenu", { }, }); +contextBridge.exposeInMainWorld("contextFileMenu", { + showMenuItemContext: () => { + ipcRenderer.invoke("show-context-menu-item"); + }, +}); + contextBridge.exposeInMainWorld("files", { openDirectoryDialog: () => ipcRenderer.invoke("open-directory-dialog"), openFileDialog: (fileExtensions?: string[]) => From 751ba17bc0357e77194137dfc81f9c119674a133 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 10 May 2024 17:14:09 -0500 Subject: [PATCH 04/12] src/components/File/index: added new handleMenuContext to display dropdown menu on React.MouseEvent Signed-off-by: MoTheRoar --- src/components/File/FileSideBar/index.tsx | 48 ++++++++++++++++++----- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/components/File/FileSideBar/index.tsx b/src/components/File/FileSideBar/index.tsx index 61fffcd1..69f2e29d 100644 --- a/src/components/File/FileSideBar/index.tsx +++ b/src/components/File/FileSideBar/index.tsx @@ -33,8 +33,13 @@ export const FileSidebar: React.FC = ({ setFileDirToBeRenamed, listHeight, }) => { + + + return ( -
+
{noteToBeRenamed && ( = ({ return visibleItems; }; + const handleMenuContext = (e: React.MouseEvent) => { + e.preventDefault(); + window.contextFileMenu.showMenuItemContext(); + } + + const hideScrollbarStyle = { + overflowY: 'auto', + scrollbarWidth: 'none', + }; + + const webKitScrollBarStyles = ` + div::-webkit-scrollbar { + display: none; + } + `; + // Calculate visible items and item count const visibleItems = getVisibleFilesAndFlatten(files, expandedDirectories); const itemCount = visibleItems.length; @@ -161,14 +182,23 @@ const FileExplorer: React.FC = ({ }; return ( - - {Rows} - + + + {Rows} + +
); + }; From dda2a8a5b4df75fa9b7949c7b2934511fc853c8b Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 10 May 2024 17:15:08 -0500 Subject: [PATCH 05/12] src/components/IconsSidebar: Added new receiver for open note/directory on menu selection. Signed-off-by: MoTheRoar --- src/components/Sidebars/IconsSidebar.tsx | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 2191af22..5745202f 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -49,6 +49,36 @@ const IconsSidebar: React.FC = ({ }; }, []); + // open a new note window + useEffect(() => { + const createNewNoteListener = window.ipcRenderer.receive( + "add-new-note-listener", + () => { + console.log("Setting new note modal to true"); + setIsNewNoteModalOpen(true); + } + ); + + return () => { + createNewNoteListener(); + } + }, []); + + // open a new directory window + useEffect(() => { + const createNewDirectoryListener = window.ipcRenderer.receive( + "add-new-directory-listener", + () => { + console.log("Adding new directory modal to true"); + setIsNewDirectoryModalOpen(true); + } + ); + + return () => { + createNewDirectoryListener(); + } + }, []); + return (
Date: Mon, 10 Jun 2024 11:23:15 -0500 Subject: [PATCH 06/12] Added highlighting for ctrl-f search within doc. Need to add next result. --- package-lock.json | 13 + package.json | 2 + src/components/Editor/SearchAndReplace.tsx | 441 ++++++++++++++++++ .../File/hooks/use-file-by-filepath.ts | 8 + src/components/FileEditorContainer.tsx | 65 ++- .../Flashcard/FlashcardMenuModal.tsx | 2 +- 6 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 src/components/Editor/SearchAndReplace.tsx diff --git a/package-lock.json b/package-lock.json index edffc757..2caf5172 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@tiptap/extension-link": "^2.2.4", "@tiptap/extension-task-item": "^2.2.4", "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-text-style": "^2.4.0", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", @@ -4149,6 +4150,18 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.4.0.tgz", + "integrity": "sha512-H0uPWeZ4sXz3o836TDWnpd38qClqzEM2d6QJ9TK+cQ1vE5Gp8wQ5W4fwUV1KAHzpJKE/15+BXBjLyVYQdmXDaQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, "node_modules/@tiptap/pm": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.2.4.tgz", diff --git a/package.json b/package.json index 73c7ddb0..0c6524bd 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@tiptap/extension-link": "^2.2.4", "@tiptap/extension-task-item": "^2.2.4", "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-text-style": "^2.4.0", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", @@ -60,6 +61,7 @@ "ollama": "^0.4.9", "openai": "^4.20.0", "posthog-js": "^1.130.2", + "prosemirror-utils": "^1.2.2", "react-card-flip": "^1.2.2", "react-icons": "^4.12.0", "react-markdown": "^9.0.1", diff --git a/src/components/Editor/SearchAndReplace.tsx b/src/components/Editor/SearchAndReplace.tsx new file mode 100644 index 00000000..d0d12a51 --- /dev/null +++ b/src/components/Editor/SearchAndReplace.tsx @@ -0,0 +1,441 @@ +// MIT License + +// Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Extension, Range, type Dispatch } from "@tiptap/core"; +import { Decoration, DecorationSet } from "@tiptap/pm/view"; +import { + Plugin, + PluginKey, + TextSelection, + type EditorState, + type Transaction, +} from "@tiptap/pm/state"; +import { Node as PMNode } from "@tiptap/pm/model"; + +declare module "@tiptap/core" { + interface Commands { + search: { + /** + * @description Set search term in extension. + */ + setSearchTerm: (searchTerm: string) => ReturnType; + /** + * @description Set replace term in extension. + */ + setReplaceTerm: (replaceTerm: string) => ReturnType; + /** + * @description Set case sensitivity in extension. + */ + setCaseSensitive: (caseSensitive: boolean) => ReturnType; + /** + * @description Reset current search result to first instance. + */ + resetIndex: () => ReturnType; + /** + * @description Find next instance of search result. + */ + nextSearchResult: () => ReturnType; + /** + * @description Find previous instance of search result. + */ + previousSearchResult: () => ReturnType; + /** + * @description Replace first instance of search result with given replace term. + */ + replace: () => ReturnType; + /** + * @description Replace all instances of search result with given replace term. + */ + replaceAll: () => ReturnType; + }; + } +} + +interface TextNodesWithPosition { + text: string; + pos: number; +} + +const getRegex = ( + s: string, + disableRegex: boolean, + caseSensitive: boolean, +): RegExp => { + return RegExp( + disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : s, + caseSensitive ? "gu" : "gui", + ); +}; + +interface ProcessedSearches { + decorationsToReturn: DecorationSet; + results: Range[]; +} + +function processSearches( + doc: PMNode, + searchTerm: RegExp, + searchResultClass: string, + resultIndex: number, +): ProcessedSearches { + const decorations: Decoration[] = []; + const results: Range[] = []; + + let textNodesWithPosition: TextNodesWithPosition[] = []; + let index = 0; + + if (!searchTerm) { + return { + decorationsToReturn: DecorationSet.empty, + results: [], + }; + } + + doc?.descendants((node, pos) => { + if (node.isText) { + if (textNodesWithPosition[index]) { + textNodesWithPosition[index] = { + text: textNodesWithPosition[index].text + node.text, + pos: textNodesWithPosition[index].pos, + }; + } else { + textNodesWithPosition[index] = { + text: `${node.text}`, + pos, + }; + } + } else { + index += 1; + } + }); + + textNodesWithPosition = textNodesWithPosition.filter(Boolean); + + for (const element of textNodesWithPosition) { + const { text, pos } = element; + const matches = Array.from(text.matchAll(searchTerm)).filter( + ([matchText]) => matchText.trim(), + ); + + for (const m of matches) { + if (m[0] === "") break; + + if (m.index !== undefined) { + results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }); + } + } + } + + for (let i = 0; i < results.length; i += 1) { + const r = results[i]; + const className = + i === resultIndex + ? `${searchResultClass} ${searchResultClass}-current` + : searchResultClass; + const decoration: Decoration = Decoration.inline(r.from, r.to, { + class: className, + }); + + decorations.push(decoration); + } + + return { + decorationsToReturn: DecorationSet.create(doc, decorations), + results, + }; +} + +const replace = ( + replaceTerm: string, + results: Range[], + { state, dispatch }: { state: EditorState; dispatch: Dispatch }, +) => { + const firstResult = results[0]; + + if (!firstResult) return; + + const { from, to } = results[0]; + + if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to)); +}; + +const rebaseNextResult = ( + replaceTerm: string, + index: number, + lastOffset: number, + results: Range[], +): [number, Range[]] | null => { + const nextIndex = index + 1; + + if (!results[nextIndex]) return null; + + const { from: currentFrom, to: currentTo } = results[index]; + + const offset = currentTo - currentFrom - replaceTerm.length + lastOffset; + + const { from, to } = results[nextIndex]; + + results[nextIndex] = { + to: to - offset, + from: from - offset, + }; + + return [offset, results]; +}; + +const replaceAll = ( + replaceTerm: string, + results: Range[], + { tr, dispatch }: { tr: Transaction; dispatch: Dispatch }, +) => { + let offset = 0; + + let resultsCopy = results.slice(); + + if (!resultsCopy.length) return; + + for (let i = 0; i < resultsCopy.length; i += 1) { + const { from, to } = resultsCopy[i]; + + tr.insertText(replaceTerm, from, to); + + const rebaseNextResultResponse = rebaseNextResult( + replaceTerm, + i, + offset, + resultsCopy, + ); + + if (!rebaseNextResultResponse) continue; + + offset = rebaseNextResultResponse[0]; + resultsCopy = rebaseNextResultResponse[1]; + } + + dispatch(tr); +}; + +export const searchAndReplacePluginKey = new PluginKey( + "searchAndReplacePlugin", +); + +export interface SearchAndReplaceOptions { + searchResultClass: string; + disableRegex: boolean; +} + +export interface SearchAndReplaceStorage { + searchTerm: string; + replaceTerm: string; + results: Range[]; + lastSearchTerm: string; + caseSensitive: boolean; + lastCaseSensitive: boolean; + resultIndex: number; + lastResultIndex: number; +} + +export const SearchAndReplace = Extension.create< + SearchAndReplaceOptions, + SearchAndReplaceStorage +>({ + name: "searchAndReplace", + + addOptions() { + return { + searchResultClass: "search-result", + disableRegex: true, + }; + }, + + addStorage() { + return { + searchTerm: "", + replaceTerm: "", + results: [], + lastSearchTerm: "", + caseSensitive: false, + lastCaseSensitive: false, + resultIndex: 0, + lastResultIndex: 0, + }; + }, + + addCommands() { + return { + setSearchTerm: + (searchTerm: string) => + ({ editor }) => { + editor.storage.searchAndReplace.searchTerm = searchTerm; + + return false; + }, + setReplaceTerm: + (replaceTerm: string) => + ({ editor }) => { + editor.storage.searchAndReplace.replaceTerm = replaceTerm; + + return false; + }, + setCaseSensitive: + (caseSensitive: boolean) => + ({ editor }) => { + editor.storage.searchAndReplace.caseSensitive = caseSensitive; + + return false; + }, + resetIndex: + () => + ({ editor }) => { + editor.storage.searchAndReplace.resultIndex = 0; + + return false; + }, + nextSearchResult: + () => + ({ editor }) => { + const { results, resultIndex } = editor.storage.searchAndReplace; + const nextIndex = resultIndex + 1 >= results.length ? 0 : resultIndex + 1; // Wrap around to the start + + if (results.length > 0) { + editor.storage.searchAndReplace.resultIndex = nextIndex; + const nextResult = results[nextIndex]; + + if (nextResult) { + const { from, to } = nextResult; + + const startPos = editor.view.state.doc.resolve(from); + const endPos = editor.view.state.doc.resolve(to); + + const transaction = editor.view.state.tr.setSelection(new TextSelection(startPos, endPos)); + editor.view.dispatch(transaction.scrollIntoView()); + editor.view.focus(); + } + } + + return true; + }, + previousSearchResult: + () => + ({ editor }) => { + const { results, resultIndex } = editor.storage.searchAndReplace; + + const prevIndex = resultIndex - 1; + + if (results[prevIndex]) { + editor.storage.searchAndReplace.resultIndex = prevIndex; + } else { + editor.storage.searchAndReplace.resultIndex = results.length - 1; + } + + return false; + }, + replace: + () => + ({ editor, state, dispatch }) => { + const { replaceTerm, results } = editor.storage.searchAndReplace; + + replace(replaceTerm, results, { state, dispatch }); + + return false; + }, + replaceAll: + () => + ({ editor, tr, dispatch }) => { + const { replaceTerm, results } = editor.storage.searchAndReplace; + + replaceAll(replaceTerm, results, { tr, dispatch }); + + return false; + }, + }; + }, + + addProseMirrorPlugins() { + const editor = this.editor; + const { searchResultClass, disableRegex } = this.options; + + const setLastSearchTerm = (t: string) => + (editor.storage.searchAndReplace.lastSearchTerm = t); + const setLastCaseSensitive = (t: boolean) => + (editor.storage.searchAndReplace.lastCaseSensitive = t); + const setLastResultIndex = (t: number) => + (editor.storage.searchAndReplace.lastResultIndex = t); + + return [ + new Plugin({ + key: searchAndReplacePluginKey, + state: { + init: () => DecorationSet.empty, + apply({ doc, docChanged }, oldState) { + const { + searchTerm, + lastSearchTerm, + caseSensitive, + lastCaseSensitive, + resultIndex, + lastResultIndex, + } = editor.storage.searchAndReplace; + + if ( + !docChanged && + lastSearchTerm === searchTerm && + lastCaseSensitive === caseSensitive && + lastResultIndex === resultIndex + ) + return oldState; + + setLastSearchTerm(searchTerm); + setLastCaseSensitive(caseSensitive); + setLastResultIndex(resultIndex); + + if (!searchTerm) { + editor.storage.searchAndReplace.results = []; + return DecorationSet.empty; + } + + const { decorationsToReturn, results } = processSearches( + doc, + getRegex(searchTerm, disableRegex, caseSensitive), + searchResultClass, + resultIndex, + ); + + editor.storage.searchAndReplace.results = results; + + return decorationsToReturn; + }, + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, + }), + ]; + }, +}); + +export default SearchAndReplace; \ No newline at end of file diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index 7b27b32c..fbb63809 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -5,6 +5,7 @@ import Document from "@tiptap/extension-document"; import Paragraph from "@tiptap/extension-paragraph"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; +import TextStyle from "@tiptap/extension-text-style"; import Text from "@tiptap/extension-text"; import "../tiptap.scss"; import { useDebounce } from "use-debounce"; @@ -21,6 +22,7 @@ import HighlightExtension, { } from "@/components/Editor/HighlightExtension"; import { toast } from "react-toastify"; import { RichTextLink } from "@/components/Editor/RichTextLink"; +import SearchAndReplace from "@/components/Editor/SearchAndReplace"; export const useFileByFilepath = () => { const [currentlyOpenedFilePath, setCurrentlyOpenedFilePath] = useState< @@ -129,6 +131,12 @@ export const useFileByFilepath = () => { Paragraph, Text, TaskList, + TextStyle, + SearchAndReplace.configure({ + searchResultClass: "bg-yellow-400", + caseSensitive: false, + disableRegex: false, + }), Markdown.configure({ html: true, // Allow HTML input/output tightLists: true, // No

inside

  • in markdown output diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index efd73af5..0fd9fe94 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import TitleBar from "./TitleBar"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; import IconsSidebar from "./Sidebars/IconsSidebar"; @@ -10,6 +10,7 @@ import InEditorBacklinkSuggestionsDisplay from "./Editor/BacklinkSuggestionsDisp import { useFileInfoTree } from "./File/FileSideBar/hooks/use-file-info-tree"; import SidebarComponent from "./Similarity/SimilarFilesSidebar"; import { useChatHistory } from "./Chat/hooks/use-chat-history"; +import { SearchInput } from "./SearchComponent"; import posthog from "posthog-js"; interface FileEditorContainerProps {} @@ -47,13 +48,17 @@ const FileEditorContainer: React.FC = () => { setShowSimilarFiles(!showSimilarFiles); }; + // const [fileIsOpen, setFileIsOpen] = useState(false); + const openFileAndOpenEditor = async (path: string) => { setShowChatbot(false); + // setFileIsOpen(true); openFileByPath(path); }; const openChatAndOpenChat = (chatHistory: ChatHistory | undefined) => { setShowChatbot(true); + // setFileIsOpen(false); setCurrentChatHistory(chatHistory); }; @@ -63,9 +68,48 @@ const FileEditorContainer: React.FC = () => { numberOfChunksToFetch: 15, }); + const [showSearch, setShowSearch] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + + // showSearch should be set to false when: + // 1) User presses ctrl-f + // 2) Navigates away from the editor + const toggleSearch = useCallback(() => { + setShowSearch(prevShowSearch => !prevShowSearch); + }) + + const handleSearchChange = (value) => { + setSearchTerm(value); + editor.commands.setSearchTerm(value); + }; + + // Global listener that triggers search functionality + useEffect(() => { + const handleKeyDown = () => { + if (event.ctrlKey && event.key === 'f') { + toggleSearch(); + } + } + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []) + + + const handleNextSearch = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (editor && editor.commands.nextSearchResult) { + editor.commands.nextSearchResult(); + } + } + } + + const handleAddFileToChatFilters = (file: string) => { setSidebarShowing("chats"); setShowChatbot(true); + setFileIsOpen(false); setCurrentChatHistory(undefined); setChatFilters((prevChatFilters) => ({ ...prevChatFilters, @@ -144,15 +188,30 @@ const FileEditorContainer: React.FC = () => { {!showChatbot && filePath && ( -
    +
    editor?.commands.focus()} style={{ backgroundColor: "rgb(30, 30, 30)", }} > + {showSearch && ( + handleSearchChange(event.target.value)} + onBlur={() => { + setShowSearch(false); + handleSearchChange(""); + }} + placeholder="Search..." + autoFocus + className="absolute top-0 right-0 mt-4 mr-4 z-50 border-none rounded-md p-2 bg-transparent bg-dark-gray-c-ten text-white" + /> + )} = ({ onClose={onClose} // tailwindStylesOnBackground="bg-gradient-to-r from-orange-900 to-yellow-900" > -
    +

    Flashcard Mode

    From f5460677bfb3b0d96a784ab109bcc3ebc461c182 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 11 Jun 2024 00:41:09 -0500 Subject: [PATCH 07/12] Added ability to search within doc --- src/components/Editor/SearchAndReplace.tsx | 31 ++++++++-------------- src/components/FileEditorContainer.tsx | 22 ++++++++++++--- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/components/Editor/SearchAndReplace.tsx b/src/components/Editor/SearchAndReplace.tsx index d0d12a51..ffe42027 100644 --- a/src/components/Editor/SearchAndReplace.tsx +++ b/src/components/Editor/SearchAndReplace.tsx @@ -316,26 +316,17 @@ export const SearchAndReplace = Extension.create< nextSearchResult: () => ({ editor }) => { - const { results, resultIndex } = editor.storage.searchAndReplace; - const nextIndex = resultIndex + 1 >= results.length ? 0 : resultIndex + 1; // Wrap around to the start - - if (results.length > 0) { - editor.storage.searchAndReplace.resultIndex = nextIndex; - const nextResult = results[nextIndex]; - - if (nextResult) { - const { from, to } = nextResult; - - const startPos = editor.view.state.doc.resolve(from); - const endPos = editor.view.state.doc.resolve(to); - - const transaction = editor.view.state.tr.setSelection(new TextSelection(startPos, endPos)); - editor.view.dispatch(transaction.scrollIntoView()); - editor.view.focus(); - } - } - - return true; + const { results, resultIndex } = editor.storage.searchAndReplace; + + const nextIndex = resultIndex + 1; + + if (results[nextIndex]) { + editor.storage.searchAndReplace.resultIndex = nextIndex; + } else { + editor.storage.searchAndReplace.resultIndex = 0; + } + + return false; }, previousSearchResult: () => diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index 0fd9fe94..d90e71d0 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -99,9 +99,23 @@ const FileEditorContainer: React.FC = () => { const handleNextSearch = (event) => { if (event.key === "Enter") { event.preventDefault(); - if (editor && editor.commands.nextSearchResult) { - editor.commands.nextSearchResult(); - } + editor.commands.nextSearchResult(); + goToSelection(); + event.target.focus(); + } + } + + const goToSelection = () => { + if (!editor) return; + + const { results, resultIndex } = editor.storage.searchAndReplace; + const position = results[resultIndex]; + if (!position) return; + + editor.commands.setTextSelection(position); + const { node } = editor.view.domAtPos(editor.state.selection.anchor); + if (node) { + (node as any).scrollIntoView?.(false); } } @@ -209,7 +223,7 @@ const FileEditorContainer: React.FC = () => { }} placeholder="Search..." autoFocus - className="absolute top-0 right-0 mt-4 mr-4 z-50 border-none rounded-md p-2 bg-transparent bg-dark-gray-c-ten text-white" + className="fixed top-8 right-64 mt-4 mr-14 z-50 border-none rounded-md p-2 bg-transparent bg-dark-gray-c-ten text-white" /> )} Date: Tue, 11 Jun 2024 10:23:55 -0500 Subject: [PATCH 08/12] Pressing escape closes search --- src/components/FileEditorContainer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index d90e71d0..83c75fbe 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -102,6 +102,9 @@ const FileEditorContainer: React.FC = () => { editor.commands.nextSearchResult(); goToSelection(); event.target.focus(); + } else if (event.key === "Escape") { + toggleSearch(); + handleSearchChange(""); } } From c01c5398d22a5f909f3a245bee22dde1bfbd0b21 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 11 Jun 2024 10:59:22 -0500 Subject: [PATCH 09/12] Added ability to search within doc --- src/components/Editor/SearchAndReplace.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Editor/SearchAndReplace.tsx b/src/components/Editor/SearchAndReplace.tsx index ffe42027..6c384549 100644 --- a/src/components/Editor/SearchAndReplace.tsx +++ b/src/components/Editor/SearchAndReplace.tsx @@ -429,4 +429,4 @@ export const SearchAndReplace = Extension.create< }, }); -export default SearchAndReplace; \ No newline at end of file +export default SearchAndReplace; From 10f9f067667e71395052ae4804b0c1f5b6b7c825 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 11 Jun 2024 13:30:11 -0500 Subject: [PATCH 10/12] Added LaTex support. Need to manually configure to add support for Matrices --- src/components/File/hooks/use-file-by-filepath.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index 7ba02dc3..de1b9b21 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -9,6 +9,7 @@ import { Editor, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { toast } from "react-toastify"; import { Markdown } from "tiptap-markdown"; +import { MathExtension } from "@aarkue/tiptap-math-extension"; import { useDebounce } from "use-debounce"; import { BacklinkExtension } from "@/components/Editor/BacklinkExtension"; @@ -22,6 +23,7 @@ import { removeFileExtension, } from "@/functions/strings"; import "../tiptap.scss"; +import "katex/dist/katex.min.css"; export const useFileByFilepath = () => { const [currentlyOpenedFilePath, setCurrentlyOpenedFilePath] = useState< @@ -130,6 +132,9 @@ export const useFileByFilepath = () => { Paragraph, Text, TaskList, + MathExtension.configure({ + evaluation: true, + }), Markdown.configure({ html: true, // Allow HTML input/output tightLists: true, // No

    inside

  • in markdown output From d875e4dba900a919bd237d6a53bdba790036d774 Mon Sep 17 00:00:00 2001 From: Mohamed Ilaiwi Date: Mon, 24 Jun 2024 00:09:05 -0500 Subject: [PATCH 11/12] Right clicking empty spot provides context menu. Can create new note and directory when rightclicking file --- electron/main/index.ts | 27 +++++++++++++++- src/components/File/NewDirectory.tsx | 19 +++++++---- src/components/File/NewNote.tsx | 15 +++++++-- src/components/Sidebars/IconsSidebar.tsx | 41 +++++++++++------------- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 8785bc59..ea92324f 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -35,6 +35,8 @@ import WindowsManager from "./windowManager"; import { errorToString } from "./Generic/error"; import { addExtensionToFilenameIfNoExtensionPresent } from "./Generic/path"; +const fs = require('fs').promises; + const store = new Store(); // store.clear(); // clear store for testing const windowsManager = new WindowsManager(); @@ -167,8 +169,31 @@ ipcMain.handle("index-files-in-directory", async (event) => { } }); -ipcMain.handle("show-context-menu-file-item", (event, file) => { +ipcMain.handle("show-context-menu-file-item", async (event, file) => { const menu = new Menu(); + const stats = await fs.stat(file.path); + const isDirectory = stats.isDirectory(); + + if (isDirectory) { + menu.append( + new MenuItem({ + label: "New Note", + click: () => { + event.sender.send("add-new-note-listener", file.relativePath); + }, + }) + ); + + menu.append( + new MenuItem({ + label: "New Directory", + click: () => { + event.sender.send("add-new-directory-listener", file.path); + }, + }) + ); + } + menu.append( new MenuItem({ label: "Delete", diff --git a/src/components/File/NewDirectory.tsx b/src/components/File/NewDirectory.tsx index 47f8f509..b8adf8cd 100644 --- a/src/components/File/NewDirectory.tsx +++ b/src/components/File/NewDirectory.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import Modal from "../Generic/Modal"; import { Button } from "@material-tailwind/react"; import { errorToString } from "@/functions/error"; @@ -8,7 +8,7 @@ import { getInvalidCharacterInFilePath } from "@/functions/strings"; interface NewDirectoryComponentProps { isOpen: boolean; onClose: () => void; - onDirectoryCreate: (path: string) => void; + onDirectoryCreate: string; } const NewDirectoryComponent: React.FC = ({ @@ -19,6 +19,13 @@ const NewDirectoryComponent: React.FC = ({ const [directoryName, setDirectoryName] = useState(""); const [errorMessage, setErrorMessage] = useState(null); + useEffect(() => { + if (!isOpen) { + setDirectoryName(""); + setErrorMessage(null); + } + }, [isOpen]); + const handleNameChange = (e: React.ChangeEvent) => { const newName = e.target.value; setDirectoryName(newName); @@ -40,12 +47,10 @@ const NewDirectoryComponent: React.FC = ({ return; } const normalizedDirectoryName = directoryName.replace(/\\/g, "/"); - const fullPath = await window.path.join( - await window.electronStore.getVaultDirectoryForWindow(), - normalizedDirectoryName - ); + const basePath = onDirectoryCreate || await window.electronStore.getVaultDirectoryForWindow(); + const fullPath = await window.path.join(basePath, normalizedDirectoryName); + window.files.createDirectory(fullPath); - onDirectoryCreate(fullPath); onClose(); } catch (e) { toast.error(errorToString(e), { diff --git a/src/components/File/NewNote.tsx b/src/components/File/NewNote.tsx index cb70bd43..8390d300 100644 --- a/src/components/File/NewNote.tsx +++ b/src/components/File/NewNote.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import Modal from "../Generic/Modal"; import { Button } from "@material-tailwind/react"; import { getInvalidCharacterInFilePath } from "@/functions/strings"; @@ -7,16 +7,25 @@ interface NewNoteComponentProps { isOpen: boolean; onClose: () => void; openRelativePath: (path: string) => void; + customFilePath: string; } const NewNoteComponent: React.FC = ({ isOpen, onClose, openRelativePath, + customFilePath, }) => { const [fileName, setFileName] = useState(""); const [errorMessage, setErrorMessage] = useState(null); + useEffect(() => { + if (!isOpen) { + setFileName(""); + setErrorMessage(null); + } + }, [isOpen]); + const handleNameChange = (e: React.ChangeEvent) => { const newName = e.target.value; setFileName(newName); @@ -36,7 +45,9 @@ const NewNoteComponent: React.FC = ({ if (!fileName || errorMessage) { return; } - openRelativePath(fileName); + const pathPrefix = customFilePath ? customFilePath.replace(/\/?$/, '/') : ''; + const fullPath = pathPrefix + fileName; + openRelativePath(fullPath); onClose(); }; diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 5745202f..c94af2b9 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -11,12 +11,12 @@ import { GrNewWindow } from "react-icons/gr"; import { LuFolderPlus } from "react-icons/lu"; import { BsChatLeftDots, BsFillChatLeftDotsFill } from "react-icons/bs"; import FlashcardMenuModal from "../Flashcard/FlashcardMenuModal"; +import { ipcRenderer } from "electron"; interface IconsSidebarProps { openRelativePath: (path: string) => void; sidebarShowing: SidebarAbleToShow; makeSidebarShow: (show: SidebarAbleToShow) => void; - filePath: string | null; } const IconsSidebar: React.FC = ({ @@ -28,6 +28,8 @@ const IconsSidebar: React.FC = ({ const [isNewNoteModalOpen, setIsNewNoteModalOpen] = useState(false); const [isNewDirectoryModalOpen, setIsNewDirectoryModalOpen] = useState(false); const [isFlashcardModeOpen, setIsFlashcardModeOpen] = useState(false); + const [customDirectoryPath, setCustomDirectoryPath] = useState(""); + const [customFilePath, setCustomFilePath] = useState(""); const [initialFileToCreateFlashcard, setInitialFileToCreateFlashcard] = useState(""); @@ -51,32 +53,26 @@ const IconsSidebar: React.FC = ({ // open a new note window useEffect(() => { - const createNewNoteListener = window.ipcRenderer.receive( - "add-new-note-listener", - () => { - console.log("Setting new note modal to true"); - setIsNewNoteModalOpen(true); - } - ); - - return () => { - createNewNoteListener(); + const handleNewNote = (relativePath: string) => { + setCustomFilePath(relativePath); + setIsNewNoteModalOpen(true); } + + window.ipcRenderer.receive("add-new-note-listener", (relativePath: string) => { + handleNewNote(relativePath); + }) }, []); // open a new directory window useEffect(() => { - const createNewDirectoryListener = window.ipcRenderer.receive( - "add-new-directory-listener", - () => { - console.log("Adding new directory modal to true"); - setIsNewDirectoryModalOpen(true); - } - ); - - return () => { - createNewDirectoryListener(); + const handleNewDirectory = (dirPath: string) => { + setCustomDirectoryPath(dirPath); + setIsNewDirectoryModalOpen(true); } + + window.ipcRenderer.receive("add-new-directory-listener", (dirPath) => { + handleNewDirectory(dirPath); + }); }, []); return ( @@ -183,11 +179,12 @@ const IconsSidebar: React.FC = ({ isOpen={isNewNoteModalOpen} onClose={() => setIsNewNoteModalOpen(false)} openRelativePath={openRelativePath} + customFilePath={customFilePath} /> setIsNewDirectoryModalOpen(false)} - onDirectoryCreate={() => console.log("Directory created")} + onDirectoryCreate={customDirectoryPath} /> {isFlashcardModeOpen && ( Date: Mon, 24 Jun 2024 00:19:26 -0500 Subject: [PATCH 12/12] Fixed posthog import --- src/components/File/NewDirectory.tsx | 2 ++ src/components/File/NewNote.tsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/File/NewDirectory.tsx b/src/components/File/NewDirectory.tsx index 627c4135..e248dacd 100644 --- a/src/components/File/NewDirectory.tsx +++ b/src/components/File/NewDirectory.tsx @@ -6,6 +6,7 @@ import Modal from "../Generic/Modal"; import { errorToString } from "@/functions/error"; import { getInvalidCharacterInFilePath } from "@/functions/strings"; +import posthog from "posthog-js"; interface NewDirectoryComponentProps { @@ -53,6 +54,7 @@ const NewDirectoryComponent: React.FC = ({ const basePath = onDirectoryCreate || await window.electronStore.getVaultDirectoryForWindow(); const fullPath = await window.path.join(basePath, normalizedDirectoryName); + posthog.capture('created_new_directory_from_new_directory_modal'); window.files.createDirectory(fullPath); onClose(); } catch (e) { diff --git a/src/components/File/NewNote.tsx b/src/components/File/NewNote.tsx index a019368c..d7757f39 100644 --- a/src/components/File/NewNote.tsx +++ b/src/components/File/NewNote.tsx @@ -1,11 +1,10 @@ import { Button } from "@material-tailwind/react"; +import posthog from "posthog-js"; import React, { useEffect, useState } from "react"; import Modal from "../Generic/Modal"; - import { getInvalidCharacterInFilePath } from "@/functions/strings"; - interface NewNoteComponentProps { isOpen: boolean; onClose: () => void; @@ -51,6 +50,7 @@ const NewNoteComponent: React.FC = ({ const pathPrefix = customFilePath ? customFilePath.replace(/\/?$/, '/') : ''; const fullPath = pathPrefix + fileName; openRelativePath(fullPath); + posthog.capture("created_new_note_from_new_note_modal"); onClose(); };