feat: add Current Editor Context option to @ context menu

- Add EditorContext to ContextMenuOptionType enum
- Update WebviewMessage types to support requestEditorContext and editorContext
- Implement backend handler in webviewMessageHandler.ts using EditorUtils.getEditorContext()
- Update ExtensionMessage interface to include editorContext field
- Add Current Editor Context option to context menu with edit icon
- Implement ChatTextArea handling for editor context selection and formatting
- Update tests to reflect new context menu option count
- Fix React Hook dependencies for proper linting

The feature allows users to type @ in chat, select 'Current Editor Context'
to insert a formatted mention containing current file context, line numbers,
and selected text from the active editor.
This commit is contained in:
Roo Code 2025-07-11 14:14:16 +00:00
parent b03d03d860
commit c813feac55
7 changed files with 118 additions and 18 deletions

View file

@ -2195,6 +2195,26 @@ export const webviewMessageHandler = async (
break
}
case "requestEditorContext": {
try {
const { EditorUtils } = await import("../../integrations/editor/EditorUtils")
const editorContext = await EditorUtils.getEditorContext()
await provider.postMessageToWebview({
type: "editorContext",
editorContext: editorContext || undefined,
})
} catch (error) {
provider.log(
`Error getting editor context: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
await provider.postMessageToWebview({
type: "editorContext",
editorContext: undefined,
})
}
break
}
case "switchTab": {
if (message.tab) {
// Capture tab shown event for all switchTab messages (which are user-initiated)

View file

@ -105,8 +105,16 @@ export interface ExtensionMessage {
| "shareTaskSuccess"
| "codeIndexSettingsSaved"
| "codeIndexSecretStatus"
| "editorContext"
text?: string
payload?: any // Add a generic payload for now, can refine later
editorContext?: {
filePath?: string
selectedText?: string
startLine?: number
endLine?: number
diagnostics?: any[]
}
action?:
| "chatButtonClicked"
| "mcpButtonClicked"

View file

@ -193,6 +193,8 @@ export interface WebviewMessage {
| "checkRulesDirectoryResult"
| "saveCodeIndexSettingsAtomic"
| "requestCodeIndexSecretStatus"
| "requestEditorContext"
| "editorContext"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
@ -252,6 +254,13 @@ export interface WebviewMessage {
codebaseIndexOpenAiCompatibleApiKey?: string
codebaseIndexGeminiApiKey?: string
}
editorContext?: {
filePath?: string
selectedText?: string
startLine?: number
endLine?: number
diagnostics?: any[]
}
}
export const checkoutDiffPayloadSchema = z.object({

View file

@ -96,6 +96,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
const [searchRequestId, setSearchRequestId] = useState<string>("")
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)
const [searchQuery, setSearchQuery] = useState("")
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
const highlightLayerRef = useRef<HTMLDivElement>(null)
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
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 [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const [isFocused, setIsFocused] = useState(false)
// Close dropdown when clicking outside.
useEffect(() => {
@ -135,28 +150,63 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (message.requestId === searchRequestId) {
setFileSearchResults(message.results || [])
}
} else if (message.type === "editorContext") {
// Handle editor context response
if (message.editorContext && textAreaRef.current) {
const editorContext = message.editorContext
let insertValue = ""
if (editorContext.filePath) {
// Format: filename:startLine-endLine (selected text preview)
const fileName = editorContext.filePath.split("/").pop() || editorContext.filePath
let contextText = fileName
if (editorContext.startLine !== undefined) {
if (
editorContext.endLine !== undefined &&
editorContext.endLine !== editorContext.startLine
) {
contextText += `:${editorContext.startLine}-${editorContext.endLine}`
} else {
contextText += `:${editorContext.startLine}`
}
}
if (editorContext.selectedText && editorContext.selectedText.trim()) {
const preview = editorContext.selectedText.trim().substring(0, 50)
contextText += ` (${preview}${editorContext.selectedText.length > 50 ? "..." : ""})`
}
insertValue = contextText
} else {
insertValue = "current-editor"
}
const { newValue, mentionIndex } = insertMention(
textAreaRef.current.value,
cursorPosition,
insertValue,
)
setInputValue(newValue)
const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
// Scroll to cursor
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.blur()
textAreaRef.current.focus()
}
}, 0)
}
}
}
window.addEventListener("message", messageHandler)
return () => window.removeEventListener("message", messageHandler)
}, [setInputValue, searchRequestId])
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
const [showContextMenu, setShowContextMenu] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)
const [searchQuery, setSearchQuery] = useState("")
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
const highlightLayerRef = useRef<HTMLDivElement>(null)
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
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 [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
const [isFocused, setIsFocused] = useState(false)
}, [setInputValue, searchRequestId, cursorPosition])
// Use custom hook for prompt history navigation
const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({
@ -277,6 +327,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
insertValue = "problems"
} else if (type === ContextMenuOptionType.Terminal) {
insertValue = "terminal"
} else if (type === ContextMenuOptionType.EditorContext) {
// Request editor context from backend
vscode.postMessage({ type: "requestEditorContext" })
setShowContextMenu(false)
setSelectedType(null)
return
} else if (type === ContextMenuOptionType.Git) {
insertValue = value || ""
}

View file

@ -91,6 +91,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
return <span>Problems</span>
case ContextMenuOptionType.Terminal:
return <span>Terminal</span>
case ContextMenuOptionType.EditorContext:
return <span>Current Editor Context</span>
case ContextMenuOptionType.URL:
return <span>Paste URL to fetch contents</span>
case ContextMenuOptionType.NoResults:
@ -173,6 +175,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
return "warning"
case ContextMenuOptionType.Terminal:
return "terminal"
case ContextMenuOptionType.EditorContext:
return "edit"
case ContextMenuOptionType.URL:
return "link"
case ContextMenuOptionType.Git:

View file

@ -196,8 +196,9 @@ describe("getContextMenuOptions", () => {
it("should return all option types for empty query", () => {
const result = getContextMenuOptions("", "", null, [])
expect(result).toHaveLength(6)
expect(result).toHaveLength(7)
expect(result.map((item) => item.type)).toEqual([
ContextMenuOptionType.EditorContext,
ContextMenuOptionType.Problems,
ContextMenuOptionType.Terminal,
ContextMenuOptionType.URL,

View file

@ -105,6 +105,7 @@ export enum ContextMenuOptionType {
Git = "git",
NoResults = "noResults",
Mode = "mode", // Add mode type
EditorContext = "editorContext", // Add editor context type
}
export interface ContextMenuQueryItem {
@ -192,6 +193,7 @@ export function getContextMenuOptions(
}
return [
{ type: ContextMenuOptionType.EditorContext },
{ type: ContextMenuOptionType.Problems },
{ type: ContextMenuOptionType.Terminal },
{ type: ContextMenuOptionType.URL },