Better command highlighting (#6336)

This commit is contained in:
Matt Rubens 2025-07-29 01:42:17 -04:00 committed by GitHub
parent e117208c29
commit 00a3738d30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 3 deletions

View file

@ -714,15 +714,41 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const text = textAreaRef.current.value
highlightLayerRef.current.innerHTML = text
// Helper function to check if a command is valid
const isValidCommand = (commandName: string): boolean => {
return commands?.some((cmd) => cmd.name === commandName) || false
}
// Process the text to highlight mentions and valid commands
let processedText = text
.replace(/\n$/, "\n\n")
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
.replace(commandRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
// Custom replacement for commands - only highlight valid ones
processedText = processedText.replace(commandRegexGlobal, (match, commandName) => {
// Only highlight if the command exists in the valid commands list
if (isValidCommand(commandName)) {
// Check if the match starts with a space
const startsWithSpace = match.startsWith(" ")
const commandPart = `/${commandName}`
if (startsWithSpace) {
// Keep the space but only highlight the command part
return ` <mark class="mention-context-textarea-highlight">${commandPart}</mark>`
} else {
// Highlight the entire command (starts at beginning of line)
return `<mark class="mention-context-textarea-highlight">${commandPart}</mark>`
}
}
return match // Return unhighlighted if command is not valid
})
highlightLayerRef.current.innerHTML = processedText
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
}, [])
}, [commands])
useLayoutEffect(() => {
updateHighlights()
@ -973,6 +999,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)}>
<div
ref={highlightLayerRef}
data-testid="highlight-layer"
className={cn(
"absolute",
"inset-0",

View file

@ -904,6 +904,144 @@ describe("ChatTextArea", () => {
})
})
describe("slash command highlighting", () => {
const mockCommands = [
{ name: "setup", source: "project", description: "Setup the project" },
{ name: "deploy", source: "global", description: "Deploy the application" },
{ name: "test-command", source: "project", description: "Test command with dash" },
]
beforeEach(() => {
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
openedTabs: [],
taskHistory: [],
cwd: "/test/workspace",
commands: mockCommands,
})
})
it("should highlight valid slash commands", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup the project" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// The highlighting is applied via innerHTML, so we need to check the content
// The valid command "/setup" should be highlighted
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
})
it("should not highlight invalid slash commands", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/invalid command" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// The invalid command "/invalid" should not be highlighted
expect(highlightLayer.innerHTML).not.toContain(
'<mark class="mention-context-textarea-highlight">/invalid</mark>',
)
// But it should still contain the text without highlighting
expect(highlightLayer.innerHTML).toContain("/invalid")
})
it("should highlight only the command portion, not arguments", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/deploy to production" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// Only "/deploy" should be highlighted, not "to production"
expect(highlightLayer.innerHTML).toContain(
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
)
expect(highlightLayer.innerHTML).not.toContain(
'<mark class="mention-context-textarea-highlight">/deploy to production</mark>',
)
})
it("should handle commands with dashes and underscores", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/test-command with args" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// The command with dash should be highlighted
expect(highlightLayer.innerHTML).toContain(
'<mark class="mention-context-textarea-highlight">/test-command</mark>',
)
})
it("should be case-sensitive when matching commands", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/Setup the project" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// "/Setup" (capital S) should not be highlighted since the command is "setup" (lowercase)
expect(highlightLayer.innerHTML).not.toContain(
'<mark class="mention-context-textarea-highlight">/Setup</mark>',
)
expect(highlightLayer.innerHTML).toContain("/Setup")
})
it("should highlight multiple valid commands in the same text", () => {
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup first then /deploy" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// Both valid commands should be highlighted
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
expect(highlightLayer.innerHTML).toContain(
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
)
})
it("should handle mixed valid and invalid commands", () => {
const { getByTestId } = render(
<ChatTextArea {...defaultProps} inputValue="/setup first then /invalid then /deploy" />,
)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// Valid commands should be highlighted
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
expect(highlightLayer.innerHTML).toContain(
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
)
// Invalid command should not be highlighted
expect(highlightLayer.innerHTML).not.toContain(
'<mark class="mention-context-textarea-highlight">/invalid</mark>',
)
expect(highlightLayer.innerHTML).toContain("/invalid")
})
it("should work when no commands are available", () => {
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
filePaths: [],
openedTabs: [],
taskHistory: [],
cwd: "/test/workspace",
commands: undefined,
})
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup the project" />)
const highlightLayer = getByTestId("highlight-layer")
expect(highlightLayer).toBeInTheDocument()
// No commands should be highlighted when commands array is undefined
expect(highlightLayer.innerHTML).not.toContain(
'<mark class="mention-context-textarea-highlight">/setup</mark>',
)
expect(highlightLayer.innerHTML).toContain("/setup")
})
})
describe("selectApiConfig", () => {
// Helper function to get the API config dropdown
const getApiConfigDropdown = () => {