mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Refactor to use ContextMenuOptionType and ContextMenuQueryItem
This commit is contained in:
@@ -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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { filePaths } = useExtensionState()
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
|
||||
const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
||||
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
||||
@@ -52,21 +54,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
|
||||
const highlightLayerRef = useRef<HTMLDivElement>(null)
|
||||
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null)
|
||||
const [selectedType, setSelectedType] = useState<ContextMenuOptionType | null>(null)
|
||||
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
|
||||
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
|
||||
const contextMenuContainerRef = useRef<HTMLDivElement>(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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}, [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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
}
|
||||
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<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
cursorPosition,
|
||||
setInputValue,
|
||||
justDeletedSpaceAfterMention,
|
||||
searchPaths,
|
||||
queryItems,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -411,7 +414,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
||||
selectedIndex={selectedMenuIndex}
|
||||
setSelectedIndex={setSelectedMenuIndex}
|
||||
selectedType={selectedType}
|
||||
searchPaths={searchPaths}
|
||||
queryItems={queryItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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<ContextMenuProps> = ({
|
||||
@@ -19,16 +19,16 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
selectedType,
|
||||
searchPaths,
|
||||
queryItems,
|
||||
}) => {
|
||||
const [filteredOptions, setFilteredOptions] = useState(
|
||||
getContextMenuOptions(searchQuery, selectedType, searchPaths)
|
||||
const [filteredOptions, setFilteredOptions] = useState<ContextMenuQueryItem[]>(
|
||||
getContextMenuOptions(searchQuery, selectedType, queryItems)
|
||||
)
|
||||
const menuRef = useRef<HTMLDivElement>(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<ContextMenuProps> = ({
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
const renderOptionContent = (option: { type: string; value: string }) => {
|
||||
switch (option.value) {
|
||||
case "file":
|
||||
case "folder":
|
||||
case "problems":
|
||||
case "url":
|
||||
case "noResults":
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{option.value === "file"
|
||||
? "Add File"
|
||||
: option.value === "folder"
|
||||
? "Add Folder"
|
||||
: option.value === "problems"
|
||||
? "Problems"
|
||||
: option.value === "url"
|
||||
? "Paste URL to scrape"
|
||||
: "No results found"}
|
||||
</span>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
direction: "rtl",
|
||||
textAlign: "left",
|
||||
unicodeBidi: "plaintext",
|
||||
}}>
|
||||
{formatFilePathForTruncation(option.value) + "\u200E"}
|
||||
</span>
|
||||
)
|
||||
const renderOptionContent = (option: ContextMenuQueryItem) => {
|
||||
switch (option.type) {
|
||||
case ContextMenuOptionType.Problems:
|
||||
return <span>Problems</span>
|
||||
case ContextMenuOptionType.URL:
|
||||
return <span>Paste URL to scrape</span>
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return <span>No results found</span>
|
||||
case ContextMenuOptionType.File:
|
||||
case ContextMenuOptionType.Folder:
|
||||
if (option.value) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
direction: "rtl",
|
||||
textAlign: "left",
|
||||
unicodeBidi: "plaintext",
|
||||
}}>
|
||||
{formatFilePathForTruncation(option.value || "") + "\u200E"}
|
||||
</span>
|
||||
)
|
||||
} else {
|
||||
return <span>Add {option.type === ContextMenuOptionType.File ? "File" : "Folder"}</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
@@ -113,50 +122,47 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
}}>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
onClick={() =>
|
||||
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)}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
minWidth: 0, // Allows child to shrink below content size
|
||||
overflow: "hidden", // Ensures content doesn't overflow
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<i
|
||||
className={`codicon codicon-${option.icon}`}
|
||||
className={`codicon codicon-${getIconForOption(option)}`}
|
||||
style={{ marginRight: "8px", flexShrink: 0, fontSize: "14px" }}
|
||||
/>
|
||||
{renderOptionContent(option)}
|
||||
</div>
|
||||
{(option.value === "file" || option.value === "folder") && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
{(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 && (
|
||||
<i
|
||||
className="codicon codicon-chevron-right"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
{(option.type === ContextMenuOptionType.Problems ||
|
||||
((option.type === ContextMenuOptionType.File ||
|
||||
option.type === ContextMenuOptionType.Folder) &&
|
||||
option.value)) && (
|
||||
<i
|
||||
className="codicon codicon-add"
|
||||
style={{ fontSize: "14px", flexShrink: 0, marginLeft: 8 }}
|
||||
|
||||
Reference in New Issue
Block a user