mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add Current Editor Selection context option to @ mentions
- Add CurrentEditorSelection to ContextMenuOptionType enum - Implement handler in webviewMessageHandler to retrieve selected text - Add hasEditorSelection state tracking in ClineProvider - Monitor editor selection changes and notify webview - Update ChatTextArea to show option when text is selected - Add proper UI rendering in ContextMenu component - Add translation keys for the new feature - Fix test mocks for onDidChangeTextEditorSelection - Fix ESLint warning by adding missing dependency to useMemo This feature allows users to easily include selected text from their editor as context in their prompts using the @ mention system, similar to other AI coding assistants. Fixes #8078
This commit is contained in:
parent
7b1e3a0ee5
commit
8523e43ca8
10 changed files with 161 additions and 6 deletions
|
|
@ -757,9 +757,17 @@ export class ClineProvider
|
|||
const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => {
|
||||
// Update subscription when workspace might have changed.
|
||||
this.updateCodeIndexStatusSubscription()
|
||||
// Update editor selection state
|
||||
this.updateEditorSelectionState()
|
||||
})
|
||||
this.webviewDisposables.push(activeEditorSubscription)
|
||||
|
||||
// Listen for selection changes in the editor
|
||||
const selectionChangeSubscription = vscode.window.onDidChangeTextEditorSelection(() => {
|
||||
this.updateEditorSelectionState()
|
||||
})
|
||||
this.webviewDisposables.push(selectionChangeSubscription)
|
||||
|
||||
// Listen for when the panel becomes visible.
|
||||
// https://github.com/microsoft/vscode-discussions/discussions/840
|
||||
if ("onDidChangeViewState" in webviewView) {
|
||||
|
|
@ -1920,9 +1928,29 @@ export class ClineProvider
|
|||
openRouterImageGenerationSelectedModel,
|
||||
openRouterUseMiddleOutTransform,
|
||||
featureRoomoteControlEnabled,
|
||||
hasEditorSelection: this.hasEditorSelection(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's currently a text selection in the active editor
|
||||
*/
|
||||
private hasEditorSelection(): boolean {
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
return !!(activeEditor && !activeEditor.selection.isEmpty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the webview with the current editor selection state
|
||||
*/
|
||||
private updateEditorSelectionState(): void {
|
||||
const hasSelection = this.hasEditorSelection()
|
||||
this.postMessageToWebview({
|
||||
type: "editorSelectionChanged",
|
||||
hasSelection,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage
|
||||
* https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ vi.mock("vscode", () => ({
|
|||
showWarningMessage: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidChangeTextEditorSelection: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
activeTextEditor: undefined,
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn().mockReturnValue({
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ vi.mock("vscode", () => ({
|
|||
showWarningMessage: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
onDidChangeActiveTextEditor: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
onDidChangeTextEditorSelection: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
activeTextEditor: undefined,
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn().mockReturnValue({
|
||||
|
|
|
|||
|
|
@ -3044,5 +3044,28 @@ export const webviewMessageHandler = async (
|
|||
})
|
||||
break
|
||||
}
|
||||
case "requestCurrentEditorSelection": {
|
||||
// Get the current editor selection
|
||||
const activeEditor = vscode.window.activeTextEditor
|
||||
if (activeEditor && !activeEditor.selection.isEmpty) {
|
||||
const selectedText = activeEditor.document.getText(activeEditor.selection)
|
||||
const fileName = activeEditor.document.fileName
|
||||
|
||||
// Insert the selected text as a mention in the chat
|
||||
await provider.postMessageToWebview({
|
||||
type: "currentEditorSelection",
|
||||
text: selectedText,
|
||||
values: { fileName },
|
||||
})
|
||||
} else {
|
||||
// No selection available
|
||||
await provider.postMessageToWebview({
|
||||
type: "currentEditorSelection",
|
||||
text: undefined,
|
||||
values: { hasSelection: false },
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ export interface ExtensionMessage {
|
|||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "dismissedUpsells"
|
||||
| "currentEditorSelection"
|
||||
| "editorSelectionChanged"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -201,6 +203,15 @@ export interface ExtensionMessage {
|
|||
commands?: Command[]
|
||||
queuedMessages?: QueuedMessage[]
|
||||
list?: string[] // For dismissedUpsells
|
||||
hasSelection?: boolean // For editorSelectionChanged
|
||||
selection?: {
|
||||
text: string
|
||||
fileName: string
|
||||
range: {
|
||||
start: { line: number; character: number }
|
||||
end: { line: number; character: number }
|
||||
}
|
||||
} | null // For currentEditorSelection
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
@ -347,6 +358,7 @@ export type ExtensionState = Pick<
|
|||
remoteControlEnabled: boolean
|
||||
taskSyncEnabled: boolean
|
||||
featureRoomoteControlEnabled: boolean
|
||||
hasEditorSelection?: boolean
|
||||
}
|
||||
|
||||
export interface ClineSayTool {
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ export interface WebviewMessage {
|
|||
| "editQueuedMessage"
|
||||
| "dismissUpsell"
|
||||
| "getDismissedUpsells"
|
||||
| "requestCurrentEditorSelection"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
taskHistory,
|
||||
clineMessages,
|
||||
commands,
|
||||
hasEditorSelection,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration.
|
||||
|
|
@ -121,7 +122,43 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
const messageHandler = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
|
||||
if (message.type === "enhancedPrompt") {
|
||||
if (message.type === "editorSelectionChanged") {
|
||||
// Update will happen through ExtensionStateContext
|
||||
// No need to handle here as hasEditorSelection comes from state
|
||||
} else if (message.type === "currentEditorSelection") {
|
||||
// Handle the editor selection response
|
||||
if (message.selection && textAreaRef.current) {
|
||||
const selection = message.selection
|
||||
const mentionText = `@${selection.fileName}:${selection.range.start.line + 1}-${selection.range.end.line + 1}`
|
||||
|
||||
// Insert the mention at the current cursor position
|
||||
const currentValue = inputValue
|
||||
const cursorPos = textAreaRef.current.selectionStart || 0
|
||||
|
||||
// Check if we need to add a space before the mention
|
||||
const textBefore = currentValue.slice(0, cursorPos)
|
||||
const needsSpaceBefore = textBefore.length > 0 && !textBefore.endsWith(" ")
|
||||
const prefix = needsSpaceBefore ? " " : ""
|
||||
|
||||
// Insert the mention at cursor position
|
||||
const newValue =
|
||||
currentValue.slice(0, cursorPos) +
|
||||
prefix +
|
||||
mentionText +
|
||||
" " +
|
||||
currentValue.slice(cursorPos)
|
||||
setInputValue(newValue)
|
||||
|
||||
// Set cursor position after the inserted mention
|
||||
const newCursorPos = cursorPos + prefix.length + mentionText.length + 1
|
||||
setTimeout(() => {
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.focus()
|
||||
textAreaRef.current.setSelectionRange(newCursorPos, newCursorPos)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
} else if (message.type === "enhancedPrompt") {
|
||||
if (message.text && textAreaRef.current) {
|
||||
try {
|
||||
// Use execCommand to replace text while preserving undo history
|
||||
|
|
@ -251,7 +288,19 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
}, [inputValue])
|
||||
|
||||
const queryItems = useMemo(() => {
|
||||
return [
|
||||
const items = []
|
||||
|
||||
// Add current editor selection if available
|
||||
if (hasEditorSelection) {
|
||||
items.push({
|
||||
type: ContextMenuOptionType.CurrentEditorSelection,
|
||||
value: "current-selection",
|
||||
label: t("chat:contextMenu.currentEditorSelection"),
|
||||
description: t("chat:contextMenu.currentEditorSelectionDescription"),
|
||||
})
|
||||
}
|
||||
|
||||
items.push(
|
||||
{ type: ContextMenuOptionType.Problems, value: "problems" },
|
||||
{ type: ContextMenuOptionType.Terminal, value: "terminal" },
|
||||
...gitCommits,
|
||||
|
|
@ -268,8 +317,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
|
||||
value: path,
|
||||
})),
|
||||
]
|
||||
}, [filePaths, gitCommits, openedTabs])
|
||||
)
|
||||
|
||||
return items
|
||||
}, [filePaths, gitCommits, openedTabs, hasEditorSelection, t])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
|
@ -326,6 +377,14 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
return
|
||||
}
|
||||
|
||||
if (type === ContextMenuOptionType.CurrentEditorSelection) {
|
||||
// Request current editor selection from the extension
|
||||
setShowContextMenu(false)
|
||||
setSelectedType(null)
|
||||
vscode.postMessage({ type: "requestCurrentEditorSelection" })
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
type === ContextMenuOptionType.File ||
|
||||
type === ContextMenuOptionType.Folder ||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,25 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
return <span>{t("chat:contextMenu.terminal")}</span>
|
||||
case ContextMenuOptionType.URL:
|
||||
return <span>{t("chat:contextMenu.url")}</span>
|
||||
case ContextMenuOptionType.CurrentEditorSelection:
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "2px" }}>
|
||||
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
|
||||
{option.description && (
|
||||
<span
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: "0.9em",
|
||||
lineHeight: "1.2",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return <span>{t("chat:contextMenu.noResults")}</span>
|
||||
case ContextMenuOptionType.Git:
|
||||
|
|
@ -230,6 +249,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
return "link"
|
||||
case ContextMenuOptionType.Git:
|
||||
return "git-commit"
|
||||
case ContextMenuOptionType.CurrentEditorSelection:
|
||||
return "selection"
|
||||
case ContextMenuOptionType.NoResults:
|
||||
return "info"
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -399,6 +399,8 @@
|
|||
"noResults": "No results",
|
||||
"problems": "Problems",
|
||||
"terminal": "Terminal",
|
||||
"url": "Paste URL to fetch contents"
|
||||
"url": "Paste URL to fetch contents",
|
||||
"currentEditorSelection": "Current Editor Selection",
|
||||
"currentEditorSelectionDescription": "Include the currently selected text from the editor"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export enum ContextMenuOptionType {
|
|||
Mode = "mode", // Add mode type
|
||||
Command = "command", // Add command type
|
||||
SectionHeader = "sectionHeader", // Add section header type
|
||||
CurrentEditorSelection = "currentEditorSelection", // Add current editor selection type
|
||||
}
|
||||
|
||||
export interface ContextMenuQueryItem {
|
||||
|
|
@ -249,7 +250,7 @@ export function getContextMenuOptions(
|
|||
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
|
||||
}
|
||||
|
||||
return [
|
||||
const defaultOptions = [
|
||||
{ type: ContextMenuOptionType.Problems },
|
||||
{ type: ContextMenuOptionType.Terminal },
|
||||
{ type: ContextMenuOptionType.URL },
|
||||
|
|
@ -257,6 +258,10 @@ export function getContextMenuOptions(
|
|||
{ type: ContextMenuOptionType.File },
|
||||
{ type: ContextMenuOptionType.Git },
|
||||
]
|
||||
|
||||
// Add CurrentEditorSelection at the top if there's an active selection
|
||||
// This will be determined by the extension host checking if there's selected text
|
||||
return defaultOptions
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue