Skip to content

Commit d1fca3b

Browse files
committed
add context item ctrl/cmd+click to open
1 parent 5c2a3e8 commit d1fca3b

File tree

2 files changed

+131
-4
lines changed

2 files changed

+131
-4
lines changed

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

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
8989
commands,
9090
} = useExtensionState()
9191

92+
// Detect if we're on Mac for modifier key detection
93+
const isMac = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0
94+
const [isModifierPressed, setIsModifierPressed] = useState(false)
95+
9296
// Find the ID and display text for the currently selected API configuration.
9397
const { currentConfigId, displayName } = useMemo(() => {
9498
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
@@ -221,6 +225,54 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
221225
setInputValue,
222226
})
223227

228+
// Handle modifier key detection for mention clicks
229+
const handleGlobalKeyDown = useCallback(
230+
(e: KeyboardEvent) => {
231+
if ((isMac && e.metaKey) || (!isMac && e.ctrlKey)) {
232+
setIsModifierPressed(true)
233+
}
234+
},
235+
[isMac],
236+
)
237+
238+
const handleGlobalKeyUp = useCallback(
239+
(e: KeyboardEvent) => {
240+
if ((isMac && !e.metaKey) || (!isMac && !e.ctrlKey)) {
241+
setIsModifierPressed(false)
242+
}
243+
},
244+
[isMac],
245+
)
246+
247+
// Add global event listeners for modifier key detection
248+
useEffect(() => {
249+
document.addEventListener("keydown", handleGlobalKeyDown)
250+
document.addEventListener("keyup", handleGlobalKeyUp)
251+
252+
return () => {
253+
document.removeEventListener("keydown", handleGlobalKeyDown)
254+
document.removeEventListener("keyup", handleGlobalKeyUp)
255+
}
256+
}, [handleGlobalKeyDown, handleGlobalKeyUp])
257+
258+
// Handle clicks on mentions in the highlight layer
259+
const handleMentionClick = useCallback(
260+
(e: MouseEvent) => {
261+
if (!isModifierPressed) return
262+
263+
const target = e.target as HTMLElement
264+
if (target.tagName === "MARK" && target.classList.contains("mention-context-textarea-highlight")) {
265+
const mentionText = target.textContent
266+
if (mentionText) {
267+
// Remove @ symbol if present and send to VSCode
268+
const cleanText = mentionText.startsWith("@") ? mentionText.slice(1) : mentionText
269+
vscode.postMessage({ type: "openMention", text: cleanText })
270+
}
271+
}
272+
},
273+
[isModifierPressed],
274+
)
275+
224276
// Fetch git commits when Git is selected or when typing a hash.
225277
useEffect(() => {
226278
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
@@ -717,11 +769,17 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
717769
return commands?.some((cmd) => cmd.name === commandName) || false
718770
}
719771

772+
// Determine the class to use based on modifier key state
773+
const mentionClass = `mention-context-textarea-highlight${isModifierPressed ? " clickable" : ""}`
774+
720775
// Process the text to highlight mentions and valid commands
721776
let processedText = text
722777
.replace(/\n$/, "\n\n")
723778
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
724-
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
779+
.replace(
780+
mentionRegexGlobal,
781+
`<mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">$&</mark>`,
782+
)
725783

726784
// Custom replacement for commands - only highlight valid ones
727785
processedText = processedText.replace(commandRegexGlobal, (match, commandName) => {
@@ -733,10 +791,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
733791

734792
if (startsWithSpace) {
735793
// Keep the space but only highlight the command part
736-
return ` <mark class="mention-context-textarea-highlight">${commandPart}</mark>`
794+
return ` <mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">${commandPart}</mark>`
737795
} else {
738796
// Highlight the entire command (starts at beginning of line)
739-
return `<mark class="mention-context-textarea-highlight">${commandPart}</mark>`
797+
return `<mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">${commandPart}</mark>`
740798
}
741799
}
742800
return match // Return unhighlighted if command is not valid
@@ -746,12 +804,23 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
746804

747805
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
748806
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
749-
}, [commands])
807+
}, [commands, isModifierPressed, isMac])
750808

751809
useLayoutEffect(() => {
752810
updateHighlights()
753811
}, [inputValue, updateHighlights])
754812

813+
// Add click event listener to highlight layer for mention clicks
814+
useEffect(() => {
815+
const highlightLayer = highlightLayerRef.current
816+
if (highlightLayer) {
817+
highlightLayer.addEventListener("click", handleMentionClick)
818+
return () => {
819+
highlightLayer.removeEventListener("click", handleMentionClick)
820+
}
821+
}
822+
}, [handleMentionClick])
823+
755824
const updateCursorPosition = useCallback(() => {
756825
if (textAreaRef.current) {
757826
setCursorPosition(textAreaRef.current.selectionStart)

webview-ui/src/index.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,12 +389,70 @@ vscode-dropdown::part(listbox) {
389389
.mention-context-highlight {
390390
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
391391
border-radius: 3px;
392+
transition: all 0.2s ease-in-out;
393+
}
394+
395+
.mention-context-highlight:hover {
396+
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
397+
transform: translateY(-1px);
398+
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
399+
}
400+
401+
.mention-context-highlight.clickable {
402+
cursor: pointer;
403+
}
404+
405+
.mention-context-highlight.clickable:hover {
406+
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
407+
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
392408
}
393409

394410
.mention-context-highlight-with-shadow {
395411
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
396412
border-radius: 3px;
397413
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
414+
transition: all 0.2s ease-in-out;
415+
}
416+
417+
.mention-context-highlight-with-shadow:hover {
418+
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
419+
transform: translateY(-1px);
420+
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
421+
}
422+
423+
.mention-context-highlight-with-shadow.clickable {
424+
cursor: pointer;
425+
}
426+
427+
.mention-context-highlight-with-shadow.clickable:hover {
428+
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
429+
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
430+
box-shadow: 0 2px 6px color-mix(in srgb, var(--vscode-textLink-foreground) 30%, transparent);
431+
}
432+
433+
.mention-context-textarea-highlight {
434+
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
435+
border-radius: 3px;
436+
transition: all 0.2s ease-in-out;
437+
cursor: default;
438+
pointer-events: none;
439+
}
440+
441+
.mention-context-textarea-highlight:hover {
442+
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
443+
transform: translateY(-1px);
444+
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
445+
}
446+
447+
.mention-context-textarea-highlight.clickable {
448+
cursor: pointer;
449+
pointer-events: auto;
450+
}
451+
452+
.mention-context-textarea-highlight.clickable:hover {
453+
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
454+
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
455+
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-textLink-foreground) 30%, transparent);
398456
}
399457

400458
/**

0 commit comments

Comments
 (0)