From 22bf10420e064af6f767245493a5f313fcd6c53d Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:13:08 -0400 Subject: [PATCH] Refactor to use ContextMenuOptionType and ContextMenuQueryItem --- webview-ui/src/components/ChatTextArea.tsx | 69 +++++----- webview-ui/src/components/ContextMenu.tsx | 146 +++++++++++---------- webview-ui/src/utils/mention-context.ts | 74 +++++------ 3 files changed, 147 insertions(+), 142 deletions(-) diff --git a/webview-ui/src/components/ChatTextArea.tsx b/webview-ui/src/components/ChatTextArea.tsx index acf3eff..cab9a2b 100644 --- a/webview-ui/src/components/ChatTextArea.tsx +++ b/webview-ui/src/components/ChatTextArea.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react" +import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import DynamicTextArea from "react-textarea-autosize" import { useExtensionState } from "../context/ExtensionStateContext" import { @@ -8,6 +8,7 @@ import { mentionRegexGlobal, removeMention, shouldShowContextMenu, + ContextMenuOptionType, } from "../utils/mention-context" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" @@ -42,6 +43,7 @@ const ChatTextArea = forwardRef( }, ref ) => { + const { filePaths } = useExtensionState() const [isTextAreaFocused, setIsTextAreaFocused] = useState(false) const [thumbnailsHeight, setThumbnailsHeight] = useState(0) const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) @@ -52,21 +54,19 @@ const ChatTextArea = forwardRef( const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false) const highlightLayerRef = useRef(null) const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) - const [selectedType, setSelectedType] = useState(null) + const [selectedType, setSelectedType] = useState(null) const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) const [intendedCursorPosition, setIntendedCursorPosition] = useState(null) const contextMenuContainerRef = useRef(null) - const { filePaths } = useExtensionState() - - const searchPaths = React.useMemo(() => { + const queryItems = useMemo(() => { return [ - { type: "problems", path: "problems" }, + { type: ContextMenuOptionType.Problems, value: "problems" }, ...filePaths .map((file) => "/" + file) .map((path) => ({ - type: path.endsWith("/") ? "folder" : "file", - path: path, + type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File, + value: path, })), ] }, [filePaths]) @@ -91,30 +91,29 @@ const ChatTextArea = forwardRef( }, [showContextMenu, setShowContextMenu]) const handleMentionSelect = useCallback( - (type: string, value: string) => { - if (type === "noResults") { + (type: ContextMenuOptionType, value?: string) => { + if (type === ContextMenuOptionType.NoResults) { return } - if (value === "file" || value === "folder") { - setSelectedType(type.toLowerCase()) - setSearchQuery("") - setSelectedMenuIndex(0) - return + if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) { + if (!value) { + setSelectedType(type) + setSearchQuery("") + setSelectedMenuIndex(0) + return + } } setShowContextMenu(false) setSelectedType(null) if (textAreaRef.current) { - let insertValue = value - if (type === "url") { - // For URLs, we insert the value as is - insertValue = value - } else if (type === "file" || type === "folder") { - // For files and folders, we insert the path - insertValue = value - } else if (type === "problems") { - // For workspace problems, we insert @problems + let insertValue = value || "" + if (type === ContextMenuOptionType.URL) { + insertValue = value || "" + } else if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) { + insertValue = value || "" + } else if (type === ContextMenuOptionType.Problems) { insertValue = "problems" } @@ -122,10 +121,8 @@ const ChatTextArea = forwardRef( setInputValue(newValue) const newCursorPosition = newValue.indexOf(" ", newValue.lastIndexOf("@")) + 1 setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) // Update intended cursor position + setIntendedCursorPosition(newCursorPosition) textAreaRef.current.focus() - // Remove the direct setSelectionRange call - // textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition) } }, [setInputValue, cursorPosition] @@ -145,14 +142,16 @@ const ChatTextArea = forwardRef( event.preventDefault() setSelectedMenuIndex((prevIndex) => { const direction = event.key === "ArrowUp" ? -1 : 1 - const options = getContextMenuOptions(searchQuery, selectedType, searchPaths) + const options = getContextMenuOptions(searchQuery, selectedType, queryItems) const optionsLength = options.length if (optionsLength === 0) return prevIndex // Find selectable options (non-URL types) const selectableOptions = options.filter( - (option) => option.type !== "url" && option.type !== "noResults" + (option) => + option.type !== ContextMenuOptionType.URL && + option.type !== ContextMenuOptionType.NoResults ) if (selectableOptions.length === 0) return -1 // No selectable options @@ -173,10 +172,14 @@ const ChatTextArea = forwardRef( } if (event.key === "Enter" && selectedMenuIndex !== -1) { event.preventDefault() - const selectedOption = getContextMenuOptions(searchQuery, selectedType, searchPaths)[ + const selectedOption = getContextMenuOptions(searchQuery, selectedType, queryItems)[ selectedMenuIndex ] - if (selectedOption && selectedOption.type !== "url" && selectedOption.type !== "noResults") { + if ( + selectedOption && + selectedOption.type !== ContextMenuOptionType.URL && + selectedOption.type !== ContextMenuOptionType.NoResults + ) { handleMentionSelect(selectedOption.type, selectedOption.value) } return @@ -236,7 +239,7 @@ const ChatTextArea = forwardRef( cursorPosition, setInputValue, justDeletedSpaceAfterMention, - searchPaths, + queryItems, ] ) @@ -411,7 +414,7 @@ const ChatTextArea = forwardRef( selectedIndex={selectedMenuIndex} setSelectedIndex={setSelectedMenuIndex} selectedType={selectedType} - searchPaths={searchPaths} + queryItems={queryItems} /> )} diff --git a/webview-ui/src/components/ContextMenu.tsx b/webview-ui/src/components/ContextMenu.tsx index 0de66ab..f5f33c3 100644 --- a/webview-ui/src/components/ContextMenu.tsx +++ b/webview-ui/src/components/ContextMenu.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useRef, useState } from "react" -import { getContextMenuOptions } from "../utils/mention-context" +import { getContextMenuOptions, ContextMenuOptionType, ContextMenuQueryItem } from "../utils/mention-context" import { formatFilePathForTruncation } from "./CodeAccordian" interface ContextMenuProps { - onSelect: (type: string, value: string) => void + onSelect: (type: ContextMenuOptionType, value?: string) => void searchQuery: string onMouseDown: () => void selectedIndex: number setSelectedIndex: (index: number) => void - selectedType: string | null - searchPaths: { type: string; path: string }[] + selectedType: ContextMenuOptionType | null + queryItems: ContextMenuQueryItem[] } const ContextMenu: React.FC = ({ @@ -19,16 +19,16 @@ const ContextMenu: React.FC = ({ selectedIndex, setSelectedIndex, selectedType, - searchPaths, + queryItems, }) => { - const [filteredOptions, setFilteredOptions] = useState( - getContextMenuOptions(searchQuery, selectedType, searchPaths) + const [filteredOptions, setFilteredOptions] = useState( + getContextMenuOptions(searchQuery, selectedType, queryItems) ) const menuRef = useRef(null) useEffect(() => { - setFilteredOptions(getContextMenuOptions(searchQuery, selectedType, searchPaths)) - }, [searchQuery, selectedType, searchPaths]) + setFilteredOptions(getContextMenuOptions(searchQuery, selectedType, queryItems)) + }, [searchQuery, selectedType, queryItems]) useEffect(() => { if (menuRef.current) { @@ -46,48 +46,57 @@ const ContextMenu: React.FC = ({ } }, [selectedIndex]) - const renderOptionContent = (option: { type: string; value: string }) => { - switch (option.value) { - case "file": - case "folder": - case "problems": - case "url": - case "noResults": - return ( - - {option.value === "file" - ? "Add File" - : option.value === "folder" - ? "Add Folder" - : option.value === "problems" - ? "Problems" - : option.value === "url" - ? "Paste URL to scrape" - : "No results found"} - - ) - default: - return ( - - {formatFilePathForTruncation(option.value) + "\u200E"} - - ) + const renderOptionContent = (option: ContextMenuQueryItem) => { + switch (option.type) { + case ContextMenuOptionType.Problems: + return Problems + case ContextMenuOptionType.URL: + return Paste URL to scrape + case ContextMenuOptionType.NoResults: + return No results found + case ContextMenuOptionType.File: + case ContextMenuOptionType.Folder: + if (option.value) { + return ( + + {formatFilePathForTruncation(option.value || "") + "\u200E"} + + ) + } else { + return Add {option.type === ContextMenuOptionType.File ? "File" : "Folder"} + } } } + const getIconForOption = (option: ContextMenuQueryItem): string => { + switch (option.type) { + case ContextMenuOptionType.File: + return "file" + case ContextMenuOptionType.Folder: + return "folder" + case ContextMenuOptionType.Problems: + return "warning" + case ContextMenuOptionType.URL: + return "link" + case ContextMenuOptionType.NoResults: + return "info" + default: + return "file" + } + } + + const isOptionSelectable = (option: ContextMenuQueryItem): boolean => { + return option.type !== ContextMenuOptionType.NoResults && option.type !== ContextMenuOptionType.URL + } + return (
= ({ }}> {filteredOptions.map((option, index) => (
- option.type !== "url" && option.type !== "noResults" && onSelect(option.type, option.value) - } + key={`${option.type}-${option.value || index}`} + onClick={() => isOptionSelectable(option) && onSelect(option.type, option.value)} style={{ padding: "8px 12px", - cursor: option.type !== "url" && option.type !== "noResults" ? "pointer" : "default", + cursor: isOptionSelectable(option) ? "pointer" : "default", color: "var(--vscode-dropdown-foreground)", borderBottom: "1px solid var(--vscode-dropdown-border)", display: "flex", alignItems: "center", justifyContent: "space-between", backgroundColor: - index === selectedIndex && option.type !== "url" && option.type !== "noResults" + index === selectedIndex && isOptionSelectable(option) ? "var(--vscode-list-activeSelectionBackground)" : "", }} - onMouseEnter={() => - option.type !== "url" && option.type !== "noResults" && setSelectedIndex(index) - }> + onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}>
{renderOptionContent(option)}
- {(option.value === "file" || option.value === "folder") && ( - - )} - {(option.type === "problems" || - ((option.type === "file" || option.type === "folder") && - option.value !== "file" && - option.value !== "folder")) && ( + {(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) && + !option.value && ( + + )} + {(option.type === ContextMenuOptionType.Problems || + ((option.type === ContextMenuOptionType.File || + option.type === ContextMenuOptionType.Folder) && + option.value)) && ( item.path.toLowerCase().includes(lowerQuery) || item.type.toLowerCase().includes(lowerQuery) -// ) -// } +export enum ContextMenuOptionType { + File = "file", + Folder = "folder", + Problems = "problems", + URL = "url", + NoResults = "noResults", +} + +export interface ContextMenuQueryItem { + type: ContextMenuOptionType + value?: string +} export function getContextMenuOptions( query: string, - selectedType: string | null = null, - searchPaths: { type: string; path: string }[] -): { type: string; value: string; icon: string }[] { + selectedType: ContextMenuOptionType | null = null, + queryItems: ContextMenuQueryItem[] +): ContextMenuQueryItem[] { if (query === "") { - if (selectedType === "file") { - const files = searchPaths - .filter((item) => item.type === "file") - .map((item) => ({ type: "file", value: item.path, icon: "file" })) - return files.length > 0 ? files : [{ type: "noResults", value: "noResults", icon: "info" }] + if (selectedType === ContextMenuOptionType.File) { + const files = queryItems + .filter((item) => item.type === ContextMenuOptionType.File) + .map((item) => ({ type: ContextMenuOptionType.File, value: item.value })) + return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }] } - if (selectedType === "folder") { - const folders = searchPaths - .filter((item) => item.type === "folder") - .map((item) => ({ type: "folder", value: item.path, icon: "folder" })) - return folders.length > 0 ? folders : [{ type: "noResults", value: "noResults", icon: "info" }] + if (selectedType === ContextMenuOptionType.Folder) { + const folders = queryItems + .filter((item) => item.type === ContextMenuOptionType.Folder) + .map((item) => ({ type: ContextMenuOptionType.Folder, value: item.value })) + return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }] } return [ - { type: "url", value: "url", icon: "link" }, - { - type: "problems", - value: "problems", - icon: "warning", - }, - { type: "folder", value: "folder", icon: "folder" }, - { type: "file", value: "file", icon: "file" }, + { type: ContextMenuOptionType.URL }, + { type: ContextMenuOptionType.Problems }, + { type: ContextMenuOptionType.Folder }, + { type: ContextMenuOptionType.File }, ] } const lowerQuery = query.toLowerCase() if (query.startsWith("http")) { - // URLs - return [{ type: "url", value: query, icon: "link" }] + return [{ type: ContextMenuOptionType.URL, value: query }] } else { - // Search for files and folders - const matchingPaths = searchPaths.filter((item) => item.path.toLowerCase().includes(lowerQuery)) + const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery)) - if (matchingPaths.length > 0) { - return matchingPaths.map((item) => ({ + if (matchingItems.length > 0) { + return matchingItems.map((item) => ({ type: item.type, - value: item.path, - icon: item.type === "file" ? "file" : item.type === "problems" ? "warning" : "folder", + value: item.value, })) } else { - return [{ type: "noResults", value: "noResults", icon: "info" }] + return [{ type: ContextMenuOptionType.NoResults }] } } }