diff --git a/package.json b/package.json index bde5b29f..df498480 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,10 @@ { "name": "Philip Park", "url": "https://github.com/philippark" + }, + { + "name": "Jeffrey Cordero", + "url": "https://github.com/jeffrey-asm" } ], "license": "MIT", diff --git a/src/client/Icons/CssSVG.tsx b/src/client/Icons/CssSVG.tsx new file mode 100644 index 00000000..52ea65c1 --- /dev/null +++ b/src/client/Icons/CssSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const CssSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/FolderSVG.tsx b/src/client/Icons/FolderSVG.tsx new file mode 100644 index 00000000..0ca60530 --- /dev/null +++ b/src/client/Icons/FolderSVG.tsx @@ -0,0 +1,19 @@ +// From Heroicons +export const FolderSVG = () => { + return ( + + + + ); + }; + \ No newline at end of file diff --git a/src/client/Icons/HtmlSVG.tsx b/src/client/Icons/HtmlSVG.tsx new file mode 100644 index 00000000..6d578ac5 --- /dev/null +++ b/src/client/Icons/HtmlSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const HtmlSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/JavaScriptSVG.tsx b/src/client/Icons/JavaScriptSVG.tsx new file mode 100644 index 00000000..0d3881e8 --- /dev/null +++ b/src/client/Icons/JavaScriptSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const JavaScriptSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/JsonSVG.tsx b/src/client/Icons/JsonSVG.tsx new file mode 100644 index 00000000..ef91d698 --- /dev/null +++ b/src/client/Icons/JsonSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const JsonSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/MarkdownSVG.tsx b/src/client/Icons/MarkdownSVG.tsx new file mode 100644 index 00000000..f3dbf142 --- /dev/null +++ b/src/client/Icons/MarkdownSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const MarkdownSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/ReactSVG.tsx b/src/client/Icons/ReactSVG.tsx new file mode 100644 index 00000000..92c94ec1 --- /dev/null +++ b/src/client/Icons/ReactSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const ReactSVG = () => { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/client/Icons/SearchFileSVG.tsx b/src/client/Icons/SearchFileSVG.tsx new file mode 100644 index 00000000..f90e66c5 --- /dev/null +++ b/src/client/Icons/SearchFileSVG.tsx @@ -0,0 +1,20 @@ +export const SearchFileSVG = () => { + return ( + + + + ); + }; + \ No newline at end of file diff --git a/src/client/Icons/SearchSVG.tsx b/src/client/Icons/SearchSVG.tsx new file mode 100644 index 00000000..62a999d3 --- /dev/null +++ b/src/client/Icons/SearchSVG.tsx @@ -0,0 +1,21 @@ +// From Heroicons +export const SearchSVG = () => { + return ( + + + + ); + }; + \ No newline at end of file diff --git a/src/client/Icons/SvelteSVG.tsx b/src/client/Icons/SvelteSVG.tsx new file mode 100644 index 00000000..426562e5 --- /dev/null +++ b/src/client/Icons/SvelteSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const SvelteSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/TypeScriptSVG.tsx b/src/client/Icons/TypeScriptSVG.tsx new file mode 100644 index 00000000..a6995e76 --- /dev/null +++ b/src/client/Icons/TypeScriptSVG.tsx @@ -0,0 +1,16 @@ +// https://reactsvgicons.com/ +export const TypeScriptSVG = () => { + return ( + + + + ); +}; diff --git a/src/client/Icons/index.tsx b/src/client/Icons/index.tsx index f44faa59..cb7c5b20 100644 --- a/src/client/Icons/index.tsx +++ b/src/client/Icons/index.tsx @@ -10,3 +10,14 @@ export { FileSVG } from './FileSVG'; export { DirectoryArrowSVG } from './DirectoryArrowSVG'; export { PlaySVG } from './PlaySVG'; export { QuestionMarkSVG } from './QuestionMarkSVG'; +export { FolderSVG } from "./FolderSVG" +export { SearchSVG } from "./SearchSVG" +export { SearchFileSVG } from "./SearchFileSVG" +export { JavaScriptSVG } from './JavaScriptSVG'; +export { TypeScriptSVG } from './TypeScriptSVG'; +export { ReactSVG } from './ReactSVG'; +export { SvelteSVG } from './SvelteSVG'; +export { HtmlSVG } from './HtmlSVG'; +export { CssSVG } from './CssSVG'; +export { JsonSVG } from './JsonSVG'; +export { MarkdownSVG } from './MarkdownSVG'; diff --git a/src/client/VZCodeContext.tsx b/src/client/VZCodeContext.tsx index 37210994..d7935ddd 100644 --- a/src/client/VZCodeContext.tsx +++ b/src/client/VZCodeContext.tsx @@ -6,13 +6,14 @@ import { useRef, } from 'react'; import { - FileId, Files, ItemId, ShareDBDoc, SubmitOperation, Username, VZCodeContent, + SearchResults, + SearchFileVisibility, } from '../types'; import { usePrettier } from './usePrettier'; import { useTypeScript } from './useTypeScript'; @@ -112,6 +113,13 @@ export type VZCodeContextValue = { typeScriptWorker: Worker | null; + search: SearchResults; + isSearchOpen: boolean; + setIsSearchOpen: (isSearchOpen: boolean) => void; + setSearch: (pattern: string) => void; + setSearchResults: (files: ShareDBDoc) => void; + setSearchFileVisibility: (files: ShareDBDoc, id: string, visibility: SearchFileVisibility) => void; + isCreateFileModalOpen: boolean; handleOpenCreateFileModal: () => void; handleCloseCreateFileModal: () => void; @@ -216,6 +224,8 @@ export const VZCodeProvider = ({ tabList, activeFileId, theme, + search, + isSearchOpen, isSettingsOpen, isDocOpen, editorWantsFocus, @@ -230,6 +240,10 @@ export const VZCodeProvider = ({ openTab, closeTabs, setTheme, + setIsSearchOpen, + setSearch, + setSearchResults, + setSearchFileVisibility, setIsSettingsOpen, setIsDocOpen, closeSettings, @@ -359,6 +373,13 @@ export const VZCodeProvider = ({ openTab, closeTabs, + search, + isSearchOpen, + setIsSearchOpen, + setSearch, + setSearchResults, + setSearchFileVisibility, + isSettingsOpen, setIsSettingsOpen, closeSettings, diff --git a/src/client/VZSidebar/Search.tsx b/src/client/VZSidebar/Search.tsx new file mode 100644 index 00000000..7488230f --- /dev/null +++ b/src/client/VZSidebar/Search.tsx @@ -0,0 +1,175 @@ +import { + JavaScriptSVG, + TypeScriptSVG, + ReactSVG, + SvelteSVG, + JsonSVG, + MarkdownSVG, + HtmlSVG, + CssSVG, + SearchFileSVG, + DirectoryArrowSVG, + CloseSVG +} from "../Icons"; +import { + useRef, + useEffect, + useContext, + useState, +} from "react"; +import { Form } from "../bootstrap"; +import { VZCodeContext } from "../VZCodeContext"; +import { SearchFile } from "../../types"; +import { EditorView } from "codemirror"; + +function getExtensionIcon(extension: string) { + switch (extension) { + case "jsx": case "tsx": + return ; + case "js": + return ; + case "ts": + return ; + case "json": + return ; + case "md": + return ; + case "html": + return ; + case "css": + return ; + case "svelte": + return ; + default: + return ; + } +} + +function jumpToPattern(editor: EditorView, pattern: string, line: number, index: number) { + const position: number = editor.state.doc.line(line).from + index; + + editor.dispatch({ + selection: { anchor: position, head: position + pattern.length }, + scrollIntoView: true, + effects: EditorView.scrollIntoView(position, { y: "center" }) + }); +} + +export const Search = () => { + const inputRef = useRef(null); + const [isMounted, setIsMounted] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const { search, setSearch, setActiveFileId, openTab, setSearchResults, setSearchFileVisibility, shareDBDoc, editorCache } = useContext(VZCodeContext); + const { pattern, results } = search; + + useEffect(() => { + if (isMounted) { + // Only conduct a search after fully mounting and entering a new non-empty pattern + setIsSearching(pattern.trim().length >= 1); + + // Search about 2 seconds after entering a new pattern + const delaySearch = setTimeout(() => { + if (pattern.trim().length >= 1 && inputRef.current) { + setSearchResults(shareDBDoc); + setIsSearching(false); + } + }, 2000); + + return () => clearTimeout(delaySearch); + } else { + setIsMounted(true); + } + }, [pattern]); + + return ( +
+ + setSearch(e.target.value)} + ref={inputRef} + spellCheck="false" + /> + + {(Object.keys(results).length >= 1 && pattern.trim().length >= 1) ? + ( +
+ { + Object.entries(results).filter(([_, file]) => file.visibility !== "closed").map(([fileId, file]: [string, SearchFile]) => ( +
+
+
+
{ + setSearchFileVisibility(shareDBDoc, fileId, file.visibility === "open" ? "flattened" : "open"); + }} + > + +
+
+ { + getExtensionIcon(file.name.split(".")[1]) + } +
{file.name}
+
+
+
+
{file.matches.length}
+
{ + setSearchFileVisibility(shareDBDoc, fileId, "closed"); + }} + > + +
+
+
+
+ {file.visibility != "flattened" && + file.matches.map((match) => { + const before = match.text.substring(0, match.index); + const hit = match.text.substring(match.index, match.index + pattern.length); + const after = match.text.substring(match.index + pattern.length); + + return ( +

{ + setActiveFileId(fileId); + openTab({ fileId: fileId, isTransient: false }); + + if (editorCache.get(fileId)) { + jumpToPattern(editorCache.get(fileId).editor, pattern, match.line, match.index); + } + }} + > + {before} + {hit} + {after} +

+ ); + }) + } +
+
+ )) + } +
+ ) : ( +
+
{isSearching ? "Searching..." : "No Results"}
+
+ ) + } +
+ ); +}; diff --git a/src/client/VZSidebar/index.tsx b/src/client/VZSidebar/index.tsx index 4e9fc876..dea737b3 100644 --- a/src/client/VZSidebar/index.tsx +++ b/src/client/VZSidebar/index.tsx @@ -5,15 +5,18 @@ import { FileTreeFile, } from '../../types'; import { Tooltip, OverlayTrigger } from '../bootstrap'; +import { Search } from "./Search"; import { getFileTree } from '../getFileTree'; import { sortFileTree } from '../sortFileTree'; import { SplitPaneResizeContext } from '../SplitPaneResizeContext'; import { + FolderSVG, + SearchSVG, BugSVG, GearSVG, NewSVG, FileSVG, - QuestionMarkSVG, + QuestionMarkSVG } from '../Icons'; import { VZCodeContext } from '../VZCodeContext'; import { Listing } from './Listing'; @@ -31,18 +34,24 @@ export const VZSidebar = ({ openSettingsTooltipText = 'Open Settings', openKeyboardShortcuts = 'Keyboard Shortcuts', reportBugTooltipText = 'Report Bug', + searchToolTipText = 'Search', + filesToolTipText = 'Files' }: { createFileTooltipText?: string; createDirTooltipText?: string; openSettingsTooltipText?: string; reportBugTooltipText?: string; openKeyboardShortcuts?: string; + searchToolTipText?: string; + filesToolTipText?: string; }) => { const { files, openTab, setIsSettingsOpen, setIsDocOpen, + isSearchOpen, + setIsSearchOpen, handleOpenCreateFileModal, handleOpenCreateDirModal, connected, @@ -103,126 +112,166 @@ export const VZSidebar = ({ onDragLeave={handleDragLeave} onDrop={handleDrop} > -
-
-
Files
-
- - {openKeyboardShortcuts} - - } +
+
+ + {filesToolTipText} + + } + > + setIsSearchOpen(false)} + className="icon-button icon-button-dark" > - - - - - - - {reportBugTooltipText} - - } + + + + + + {searchToolTipText} + + } + > + setIsSearchOpen(true)} + className="icon-button icon-button-dark" > - - - - - - - - - {openSettingsTooltipText} - - } + + + + + + {openKeyboardShortcuts} + + } + > + - - - - - - - {createFileTooltipText} - - } + + + + + + {reportBugTooltipText} + + } + > + - - + + - - - {/*Directory Rename*/} - - {createDirTooltipText} - - } + + + + + {openSettingsTooltipText} + + } + > + - - - - -
+ + + + + + {createFileTooltipText} + + } + > + + + + + + {/*Directory Rename*/} + + {createDirTooltipText} + + } + > + + + +
- {isDragOver ? ( -
-
- Drop files here! + +
+ {!(isSearchOpen) ? ( +
+ {isDragOver ? ( +
+
+ Drop files here! +
+
+ ) : filesExist ? ( + fileTree.children.map((entity) => { + const { fileId } = entity as FileTreeFile; + const { path } = entity as FileTree; + const key = fileId ? fileId : path; + return ( + + ); + }) + ) : ( +
+
+ It looks like you don't have any files yet! + Click the "Create file" button above to create + your first file. +
+
+ )}
-
- ) : filesExist ? ( - fileTree.children.map((entity) => { - const { fileId } = entity as FileTreeFile; - const { path } = entity as FileTree; - const key = fileId ? fileId : path; - return ( - - ); - }) - ) : ( -
-
- It looks like you don't have any files yet! - Click the "Create file" button above to create - your first file. + ) : ( +
+
-
- )} + )} +
{enableConnectionStatus && ( @@ -230,9 +279,8 @@ export const VZSidebar = ({ {connected ? 'Connected' : 'Connection Lost'}
diff --git a/src/client/VZSidebar/styles.scss b/src/client/VZSidebar/styles.scss index 947d4024..bc850869 100644 --- a/src/client/VZSidebar/styles.scss +++ b/src/client/VZSidebar/styles.scss @@ -7,20 +7,26 @@ justify-content: space-between; .full-box { + width:100%; + height: 100%; padding: 4px; display: flex; flex-direction: row; - align-items: center; - justify-content: space-between; + justify-content: start; + align-items: start; border-bottom: 2px solid var(--vh-color-neutral-02); + overflow: hidden; } .files { - overflow: auto; display: flex; - flex-direction: column; + flex-direction: row; + justify-content: start; + align-items: start; flex: 1; outline: none; + overflow: auto; + height: 100%; } .file-or-directory { @@ -46,14 +52,95 @@ } .sidebar-section-buttons { + border-right: 2px solid var(--vh-color-neutral-02); + height: 100%; display: flex; - flex-direction: row; + flex-direction: column; align-items: center; - a { + gap: 8px; + padding: 8px; + + * { + color: inherit; + } + } + + .sidebar-files, .sidebar-search { + width:100%; + } + + .sidebar-search { + padding: 10px; + + * { color: inherit; } } + .search-result-lines { + margin-top: 10px; + margin-left: 20px; + } + + .search-file-heading { + display: flex; + justify-content: space-between; + align-items: center; + + * { + margin: 0px; + } + } + + .search-file-title, .search-file-name, .search-file-info { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + } + + .search-file-name { + font-size: inherit; + } + + .search-file-count { + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + margin: 0px; + font-size: 12px; + font-weight: bold; + text-align: center; + background: var(--vh-color-neutral-04); + color: var(--vh-color-neutral-01); + border-radius: 100%; + } + + .search-results { + padding: 10px; + } + + .search-file-lines { + font-size: inherit; + margin: 15px 0px 0px 30px; + } + + .search-line, .arrow-wrapper, .search-file-close { + &:hover { + opacity: 0.7; + cursor: pointer; + } + } + + .search-file-lines .search-pattern { + background: var(--vh-color-neutral-04); + color: var(--vh-color-neutral-01); + margin: 0.5px; + padding: 1px; + } + .new-btn { justify-content: right; margin-right: 10px; diff --git a/src/client/useActions.ts b/src/client/useActions.ts index 2d9d21bf..211f8d79 100644 --- a/src/client/useActions.ts +++ b/src/client/useActions.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { ThemeLabel } from './themes'; -import { FileId, Username } from '../types'; +import { FileId, SearchFileVisibility, ShareDBDoc, Username, VZCodeContent } from '../types'; import { TabState, VZAction } from './vzReducer'; // This is a custom hook that returns a set of functions @@ -57,6 +57,52 @@ export const useActions = ( [dispatch], ); + // True to show the settings modal. + const setIsSearchOpen = useCallback( + (value: boolean) => { + dispatch({ + type: 'set_is_search_open', + value: value, + }); + }, + [dispatch], + ); + + // Update search pattern + const setSearch = useCallback( + (pattern: string) => { + dispatch({ + type: 'set_search', + value: pattern, + }); + }, + [dispatch], + ); + + // Update search results based on current pattern + const setSearchResults = useCallback( + (files: ShareDBDoc) => { + dispatch({ + type: 'set_search_results', + files: files, + }); + }, + [dispatch], + ); + + // Update search results file visibility based on current pattern + const setSearchFileVisibility = useCallback( + (files: ShareDBDoc, id: string, visibility: SearchFileVisibility) => { + dispatch({ + type: 'set_search_file_visibility', + files: files, + id: id, + visibility: visibility + }); + }, + [dispatch], + ); + // True to show the settings modal. const setIsSettingsOpen = useCallback( (value: boolean) => { @@ -109,6 +155,10 @@ export const useActions = ( openTab, closeTabs, setTheme, + setIsSearchOpen, + setSearch, + setSearchResults, + setSearchFileVisibility, setIsSettingsOpen, setIsDocOpen, closeSettings, diff --git a/src/client/useFileCRUD.ts b/src/client/useFileCRUD.ts index dc886e7e..f335eed0 100644 --- a/src/client/useFileCRUD.ts +++ b/src/client/useFileCRUD.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { FileId, FileTreePath, diff --git a/src/client/vzReducer/createInitialState.ts b/src/client/vzReducer/createInitialState.ts index 8a46f1e1..a8f1f98c 100644 --- a/src/client/vzReducer/createInitialState.ts +++ b/src/client/vzReducer/createInitialState.ts @@ -12,6 +12,8 @@ export const createInitialState = ({ tabList: [], activeFileId: null, theme: defaultTheme, + search: { pattern: "", results: {}}, + isSearchOpen: false, isSettingsOpen: false, isDocOpen: false, editorWantsFocus: false, diff --git a/src/client/vzReducer/index.ts b/src/client/vzReducer/index.ts index 635ac1d0..d1e01fb4 100644 --- a/src/client/vzReducer/index.ts +++ b/src/client/vzReducer/index.ts @@ -1,4 +1,4 @@ -import { FileId, Username } from '../../types'; +import { FileId, SearchFileVisibility, SearchResults, ShareDBDoc, Username, VZCodeContent } from '../../types'; import { ThemeLabel } from '../themes'; import { closeTabsReducer } from './closeTabsReducer'; import { openTabReducer } from './openTabReducer'; @@ -12,7 +12,7 @@ import { setIsDocOpenReducer } from './setIsDocOpenReducer'; import { setThemeReducer } from './setThemeReducer'; import { editorNoLongerWantsFocusReducer } from './editorNoLongerWantsFocusReducer'; import { setUsernameReducer } from './setUsernameReducer'; - +import { setIsSearchOpenReducer, setSearchReducer, setSearchResultsReducer, setSearchFileVisibilityReducer } from './searchReducer'; export { createInitialState } from './createInitialState'; // The shape of the state managed by the reducer. @@ -26,6 +26,12 @@ export type VZState = { // The theme that is currently active. theme: ThemeLabel; + + // Search pattern and most recent results based on the current pattern + search: SearchResults + + // True to show the search instead of files + isSearchOpen: boolean; // True to show the settings modal. isSettingsOpen: boolean; @@ -73,6 +79,22 @@ export type VZAction = | { type: 'set_is_settings_open'; value: boolean } | { type: 'set_is_doc_open'; value: boolean } + // `set_is_search_open` + // * Sets whether the search tab is open. + | { type: 'set_is_search_open'; value: boolean } + + // `set_search` + // * Sets the current search pattern + | { type: 'set_search'; value: string; } + + // `set_search_results` + // * Sets the current search pattern + | { type: 'set_search_results'; files: ShareDBDoc } + + // `set_search_results_visibility` + // * Sets the visibility of a current search pattern file + | { type: 'set_search_file_visibility'; files: ShareDBDoc; id: string; visibility: SearchFileVisibility } + // `editor_no_longer_wants_focus` // * Sets `editorWantsFocus` to `false`. | { type: 'editor_no_longer_wants_focus' } @@ -107,6 +129,10 @@ const reducers = [ openTabReducer, closeTabsReducer, setThemeReducer, + setSearchReducer, + setSearchResultsReducer, + setSearchFileVisibilityReducer, + setIsSearchOpenReducer, setIsSettingsOpenReducer, setIsDocOpenReducer, editorNoLongerWantsFocusReducer, diff --git a/src/client/vzReducer/searchReducer.ts b/src/client/vzReducer/searchReducer.ts new file mode 100644 index 00000000..99c9e64f --- /dev/null +++ b/src/client/vzReducer/searchReducer.ts @@ -0,0 +1,68 @@ +import { VZAction, VZState } from '.'; +import { SearchFile, SearchFileVisibility, SearchResult, ShareDBDoc, VZCodeContent } from '../../types'; + +function searchPattern(shareDBDoc: ShareDBDoc, pattern: string): SearchResult { + const files = shareDBDoc.data.files + const fileIds = Object.keys(shareDBDoc.data.files); + let results: { [id: string] : SearchFile } = {}; + + for (let i = 0; i < fileIds.length; i++) { + const file = files[fileIds[i]]; + + if (files[fileIds[i]].text) { + const fileName = file.name; + const lines = file.text.split("\n"); + const matches = []; + + for (let j = 0; j < lines.length; j++) { + const index = lines[j].indexOf(pattern);; + + if (index !== -1) { + matches.push({ line: j + 1, index: index, text: lines[j] }); + } + } + + if (matches.length > 0) { + results[fileIds[i]] = { name: fileName, matches: matches, visibility: "open" }; + } + } + } + + return results; +} + +function updateSearchFileVisibility(results: SearchResult, id: string, visibility: SearchFileVisibility): SearchResult { + return { ...results, [id]: { ...results[id], visibility: visibility }}; +} + +export const setIsSearchOpenReducer = ( + state: VZState, + action: VZAction, +): VZState => + action.type === 'set_is_search_open' + ? { ...state, isSearchOpen: action.value } + : state; + +export const setSearchReducer = ( + state: VZState, + action: VZAction, +): VZState => + action.type === 'set_search' + ? { ...state, search: { pattern: action.value, results: {} } } + : state; + +export const setSearchResultsReducer = ( + state: VZState, + action: VZAction, + ): VZState => + action.type === 'set_search_results' + ? { ...state, search: { pattern: state.search.pattern, results: searchPattern(action.files,state.search.pattern) } } + : state; + +export const setSearchFileVisibilityReducer = ( + state: VZState, + action: VZAction, +): VZState => + action.type === 'set_search_file_visibility' + ? { ...state, search: { pattern: state.search.pattern, results: updateSearchFileVisibility(state.search.results, action.id, action.visibility)} } + : state; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 99165686..f6d77dd7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,26 @@ export interface FileTreeFile { file: File; fileId: FileId; } +export type SearchMatch = Array<{ + line: number; + index: number; + text: string; +}>; +export type SearchFileVisibility = + | 'open' + | 'flattened' + | 'closed'; +export type SearchResult = { [id: string]: SearchFile }; + +export interface SearchFile { + name: string; + matches: SearchMatch; + visibility: SearchFileVisibility; +} +export interface SearchResults { + pattern: string; + results: SearchResult; +} export type JSONOp = any;