Skip to content

Commit c813fea

Browse files
committed
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.
1 parent b03d03d commit c813fea

File tree

7 files changed

+118
-18
lines changed

7 files changed

+118
-18
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2195,6 +2195,26 @@ export const webviewMessageHandler = async (
21952195
break
21962196
}
21972197

2198+
case "requestEditorContext": {
2199+
try {
2200+
const { EditorUtils } = await import("../../integrations/editor/EditorUtils")
2201+
const editorContext = await EditorUtils.getEditorContext()
2202+
2203+
await provider.postMessageToWebview({
2204+
type: "editorContext",
2205+
editorContext: editorContext || undefined,
2206+
})
2207+
} catch (error) {
2208+
provider.log(
2209+
`Error getting editor context: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
2210+
)
2211+
await provider.postMessageToWebview({
2212+
type: "editorContext",
2213+
editorContext: undefined,
2214+
})
2215+
}
2216+
break
2217+
}
21982218
case "switchTab": {
21992219
if (message.tab) {
22002220
// Capture tab shown event for all switchTab messages (which are user-initiated)

src/shared/ExtensionMessage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,16 @@ export interface ExtensionMessage {
105105
| "shareTaskSuccess"
106106
| "codeIndexSettingsSaved"
107107
| "codeIndexSecretStatus"
108+
| "editorContext"
108109
text?: string
109110
payload?: any // Add a generic payload for now, can refine later
111+
editorContext?: {
112+
filePath?: string
113+
selectedText?: string
114+
startLine?: number
115+
endLine?: number
116+
diagnostics?: any[]
117+
}
110118
action?:
111119
| "chatButtonClicked"
112120
| "mcpButtonClicked"

src/shared/WebviewMessage.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export interface WebviewMessage {
193193
| "checkRulesDirectoryResult"
194194
| "saveCodeIndexSettingsAtomic"
195195
| "requestCodeIndexSecretStatus"
196+
| "requestEditorContext"
197+
| "editorContext"
196198
text?: string
197199
editedMessageContent?: string
198200
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
@@ -252,6 +254,13 @@ export interface WebviewMessage {
252254
codebaseIndexOpenAiCompatibleApiKey?: string
253255
codebaseIndexGeminiApiKey?: string
254256
}
257+
editorContext?: {
258+
filePath?: string
259+
selectedText?: string
260+
startLine?: number
261+
endLine?: number
262+
diagnostics?: any[]
263+
}
255264
}
256265

257266
export const checkoutDiffPayloadSchema = z.object({

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
9696
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
9797
const [searchLoading, setSearchLoading] = useState(false)
9898
const [searchRequestId, setSearchRequestId] = useState<string>("")
99+
const [isDraggingOver, setIsDraggingOver] = useState(false)
100+
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
101+
const [showContextMenu, setShowContextMenu] = useState(false)
102+
const [cursorPosition, setCursorPosition] = useState(0)
103+
const [searchQuery, setSearchQuery] = useState("")
104+
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
105+
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
106+
const highlightLayerRef = useRef<HTMLDivElement>(null)
107+
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
108+
const [selectedType, setSelectedType] = useState<ContextMenuOptionType | null>(null)
109+
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
110+
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
111+
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
112+
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
113+
const [isFocused, setIsFocused] = useState(false)
99114

100115
// Close dropdown when clicking outside.
101116
useEffect(() => {
@@ -135,28 +150,63 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
135150
if (message.requestId === searchRequestId) {
136151
setFileSearchResults(message.results || [])
137152
}
153+
} else if (message.type === "editorContext") {
154+
// Handle editor context response
155+
if (message.editorContext && textAreaRef.current) {
156+
const editorContext = message.editorContext
157+
let insertValue = ""
158+
159+
if (editorContext.filePath) {
160+
// Format: filename:startLine-endLine (selected text preview)
161+
const fileName = editorContext.filePath.split("/").pop() || editorContext.filePath
162+
let contextText = fileName
163+
164+
if (editorContext.startLine !== undefined) {
165+
if (
166+
editorContext.endLine !== undefined &&
167+
editorContext.endLine !== editorContext.startLine
168+
) {
169+
contextText += `:${editorContext.startLine}-${editorContext.endLine}`
170+
} else {
171+
contextText += `:${editorContext.startLine}`
172+
}
173+
}
174+
175+
if (editorContext.selectedText && editorContext.selectedText.trim()) {
176+
const preview = editorContext.selectedText.trim().substring(0, 50)
177+
contextText += ` (${preview}${editorContext.selectedText.length > 50 ? "..." : ""})`
178+
}
179+
180+
insertValue = contextText
181+
} else {
182+
insertValue = "current-editor"
183+
}
184+
185+
const { newValue, mentionIndex } = insertMention(
186+
textAreaRef.current.value,
187+
cursorPosition,
188+
insertValue,
189+
)
190+
191+
setInputValue(newValue)
192+
const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1
193+
setCursorPosition(newCursorPosition)
194+
setIntendedCursorPosition(newCursorPosition)
195+
196+
// Scroll to cursor
197+
setTimeout(() => {
198+
if (textAreaRef.current) {
199+
textAreaRef.current.blur()
200+
textAreaRef.current.focus()
201+
}
202+
}, 0)
203+
}
138204
}
139205
}
140206

141207
window.addEventListener("message", messageHandler)
142208
return () => window.removeEventListener("message", messageHandler)
143-
}, [setInputValue, searchRequestId])
144-
145-
const [isDraggingOver, setIsDraggingOver] = useState(false)
146-
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
147-
const [showContextMenu, setShowContextMenu] = useState(false)
148-
const [cursorPosition, setCursorPosition] = useState(0)
149-
const [searchQuery, setSearchQuery] = useState("")
150-
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
151-
const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false)
152-
const highlightLayerRef = useRef<HTMLDivElement>(null)
153-
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
154-
const [selectedType, setSelectedType] = useState<ContextMenuOptionType | null>(null)
155-
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
156-
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
157-
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
158-
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
159-
const [isFocused, setIsFocused] = useState(false)
209+
}, [setInputValue, searchRequestId, cursorPosition])
160210

161211
// Use custom hook for prompt history navigation
162212
const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({
@@ -277,6 +327,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
277327
insertValue = "problems"
278328
} else if (type === ContextMenuOptionType.Terminal) {
279329
insertValue = "terminal"
330+
} else if (type === ContextMenuOptionType.EditorContext) {
331+
// Request editor context from backend
332+
vscode.postMessage({ type: "requestEditorContext" })
333+
setShowContextMenu(false)
334+
setSelectedType(null)
335+
return
280336
} else if (type === ContextMenuOptionType.Git) {
281337
insertValue = value || ""
282338
}

webview-ui/src/components/chat/ContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
9191
return <span>Problems</span>
9292
case ContextMenuOptionType.Terminal:
9393
return <span>Terminal</span>
94+
case ContextMenuOptionType.EditorContext:
95+
return <span>Current Editor Context</span>
9496
case ContextMenuOptionType.URL:
9597
return <span>Paste URL to fetch contents</span>
9698
case ContextMenuOptionType.NoResults:
@@ -173,6 +175,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
173175
return "warning"
174176
case ContextMenuOptionType.Terminal:
175177
return "terminal"
178+
case ContextMenuOptionType.EditorContext:
179+
return "edit"
176180
case ContextMenuOptionType.URL:
177181
return "link"
178182
case ContextMenuOptionType.Git:

webview-ui/src/utils/__tests__/context-mentions.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ describe("getContextMenuOptions", () => {
196196

197197
it("should return all option types for empty query", () => {
198198
const result = getContextMenuOptions("", "", null, [])
199-
expect(result).toHaveLength(6)
199+
expect(result).toHaveLength(7)
200200
expect(result.map((item) => item.type)).toEqual([
201+
ContextMenuOptionType.EditorContext,
201202
ContextMenuOptionType.Problems,
202203
ContextMenuOptionType.Terminal,
203204
ContextMenuOptionType.URL,

webview-ui/src/utils/context-mentions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export enum ContextMenuOptionType {
105105
Git = "git",
106106
NoResults = "noResults",
107107
Mode = "mode", // Add mode type
108+
EditorContext = "editorContext", // Add editor context type
108109
}
109110

110111
export interface ContextMenuQueryItem {
@@ -192,6 +193,7 @@ export function getContextMenuOptions(
192193
}
193194

194195
return [
196+
{ type: ContextMenuOptionType.EditorContext },
195197
{ type: ContextMenuOptionType.Problems },
196198
{ type: ContextMenuOptionType.Terminal },
197199
{ type: ContextMenuOptionType.URL },

0 commit comments

Comments
 (0)