mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Support inserting mentions after a slash command (#6327)
* Support inserting mentions after a slash command * Highlight slash commands
This commit is contained in:
parent
cd26fc493f
commit
e9561743a3
5 changed files with 97 additions and 74 deletions
|
|
@ -18,7 +18,7 @@ describe("Command Autocomplete", () => {
|
|||
|
||||
describe("slash command command suggestions", () => {
|
||||
it('should return all commands when query is just "/"', () => {
|
||||
const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should have 6 items: 1 section header + 5 commands
|
||||
expect(options).toHaveLength(6)
|
||||
|
|
@ -36,7 +36,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should filter commands based on fuzzy search", () => {
|
||||
const options = getContextMenuOptions("/set", "/set", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/set", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should match 'setup' (fuzzy search behavior may vary)
|
||||
expect(options.length).toBeGreaterThan(0)
|
||||
|
|
@ -46,7 +46,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should return commands with correct format", () => {
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
const setupOption = options.find((option) => option.value === "setup")
|
||||
expect(setupOption).toBeDefined()
|
||||
|
|
@ -56,7 +56,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should handle empty command list", () => {
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], [])
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], [])
|
||||
|
||||
// Should return NoResults when no commands match
|
||||
expect(options).toHaveLength(1)
|
||||
|
|
@ -64,15 +64,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should handle no matching commands", () => {
|
||||
const options = getContextMenuOptions(
|
||||
"/nonexistent",
|
||||
"/nonexistent",
|
||||
null,
|
||||
mockQueryItems,
|
||||
[],
|
||||
[],
|
||||
mockCommands,
|
||||
)
|
||||
const options = getContextMenuOptions("/nonexistent", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should return NoResults when no commands match
|
||||
expect(options).toHaveLength(1)
|
||||
|
|
@ -80,7 +72,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should not return command suggestions for non-slash queries", () => {
|
||||
const options = getContextMenuOptions("setup", "setup", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("setup", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should not contain command options for non-slash queries
|
||||
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
|
||||
|
|
@ -94,7 +86,7 @@ describe("Command Autocomplete", () => {
|
|||
{ name: "deploy.prod", source: "global" },
|
||||
]
|
||||
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], specialCommands)
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], specialCommands)
|
||||
|
||||
const setupDevOption = options.find((option) => option.value === "setup-dev")
|
||||
expect(setupDevOption).toBeDefined()
|
||||
|
|
@ -102,7 +94,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should handle case-insensitive fuzzy matching", () => {
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
const commandNames = options.map((option) => option.value)
|
||||
expect(commandNames).toContain("setup")
|
||||
|
|
@ -115,15 +107,7 @@ describe("Command Autocomplete", () => {
|
|||
{ name: "integration-test", source: "project" },
|
||||
]
|
||||
|
||||
const options = getContextMenuOptions(
|
||||
"/test",
|
||||
"/test",
|
||||
null,
|
||||
mockQueryItems,
|
||||
[],
|
||||
[],
|
||||
commandsWithSimilarNames,
|
||||
)
|
||||
const options = getContextMenuOptions("/test", null, mockQueryItems, [], [], commandsWithSimilarNames)
|
||||
|
||||
// Filter out section headers and check the first command
|
||||
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
|
||||
|
|
@ -131,7 +115,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should handle partial matches correctly", () => {
|
||||
const options = getContextMenuOptions("/te", "/te", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/te", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should match 'test-suite'
|
||||
const commandNames = options.map((option) => option.value)
|
||||
|
|
@ -158,7 +142,7 @@ describe("Command Autocomplete", () => {
|
|||
] as any[]
|
||||
|
||||
it("should return both modes and commands for slash commands", () => {
|
||||
const options = getContextMenuOptions("/", "/", null, mockQueryItems, [], mockModes, mockCommands)
|
||||
const options = getContextMenuOptions("/", null, mockQueryItems, [], mockModes, mockCommands)
|
||||
|
||||
const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode)
|
||||
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
|
||||
|
|
@ -168,7 +152,7 @@ describe("Command Autocomplete", () => {
|
|||
})
|
||||
|
||||
it("should filter both modes and commands based on query", () => {
|
||||
const options = getContextMenuOptions("/co", "/co", null, mockQueryItems, [], mockModes, mockCommands)
|
||||
const options = getContextMenuOptions("/co", null, mockQueryItems, [], mockModes, mockCommands)
|
||||
|
||||
// Should match 'code' mode and possibly some commands (fuzzy search may match)
|
||||
const modeOptions = options.filter((option) => option.type === ContextMenuOptionType.Mode)
|
||||
|
|
@ -183,7 +167,7 @@ describe("Command Autocomplete", () => {
|
|||
|
||||
describe("command source indication", () => {
|
||||
it("should not expose source information in autocomplete", () => {
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
const setupOption = options.find((option) => option.value === "setup")
|
||||
expect(setupOption).toBeDefined()
|
||||
|
|
@ -199,14 +183,14 @@ describe("Command Autocomplete", () => {
|
|||
|
||||
describe("edge cases", () => {
|
||||
it("should handle undefined commands gracefully", () => {
|
||||
const options = getContextMenuOptions("/setup", "/setup", null, mockQueryItems, [], [], undefined)
|
||||
const options = getContextMenuOptions("/setup", null, mockQueryItems, [], [], undefined)
|
||||
|
||||
expect(options).toHaveLength(1)
|
||||
expect(options[0].type).toBe(ContextMenuOptionType.NoResults)
|
||||
})
|
||||
|
||||
it("should handle empty query with commands", () => {
|
||||
const options = getContextMenuOptions("", "", null, mockQueryItems, [], [], mockCommands)
|
||||
const options = getContextMenuOptions("", null, mockQueryItems, [], [], mockCommands)
|
||||
|
||||
// Should not return command options for empty query
|
||||
const commandOptions = options.filter((option) => option.type === ContextMenuOptionType.Command)
|
||||
|
|
@ -218,7 +202,7 @@ describe("Command Autocomplete", () => {
|
|||
{ name: "very-long-command-name-that-exceeds-normal-length", source: "project" },
|
||||
]
|
||||
|
||||
const options = getContextMenuOptions("/very", "/very", null, mockQueryItems, [], [], longNameCommands)
|
||||
const options = getContextMenuOptions("/very", null, mockQueryItems, [], [], longNameCommands)
|
||||
|
||||
// Should have 2 items: 1 section header + 1 command
|
||||
expect(options.length).toBe(2)
|
||||
|
|
@ -233,7 +217,7 @@ describe("Command Autocomplete", () => {
|
|||
{ name: "123test", source: "project" },
|
||||
]
|
||||
|
||||
const options = getContextMenuOptions("/v", "/v", null, mockQueryItems, [], [], numericCommands)
|
||||
const options = getContextMenuOptions("/v", null, mockQueryItems, [], [], numericCommands)
|
||||
|
||||
const commandNames = options.map((option) => option.value)
|
||||
expect(commandNames).toContain("v2-setup")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, us
|
|||
import { useEvent } from "react-use"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
|
||||
import { mentionRegex, mentionRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
|
||||
import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions"
|
||||
import { WebviewMessage } from "@roo/WebviewMessage"
|
||||
import { Mode, getAllModes } from "@roo/modes"
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
|
|
@ -356,10 +356,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
insertValue = value ? `/${value}` : ""
|
||||
}
|
||||
|
||||
// Determine if this is a slash command selection
|
||||
const isSlashCommand = type === ContextMenuOptionType.Mode || type === ContextMenuOptionType.Command
|
||||
|
||||
const { newValue, mentionIndex } = insertMention(
|
||||
textAreaRef.current.value,
|
||||
cursorPosition,
|
||||
insertValue,
|
||||
isSlashCommand,
|
||||
)
|
||||
|
||||
setInputValue(newValue)
|
||||
|
|
@ -395,7 +399,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||
const options = getContextMenuOptions(
|
||||
searchQuery,
|
||||
inputValue,
|
||||
selectedType,
|
||||
queryItems,
|
||||
fileSearchResults,
|
||||
|
|
@ -434,7 +437,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
event.preventDefault()
|
||||
const selectedOption = getContextMenuOptions(
|
||||
searchQuery,
|
||||
inputValue,
|
||||
selectedType,
|
||||
queryItems,
|
||||
fileSearchResults,
|
||||
|
|
@ -557,7 +559,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
setShowContextMenu(showMenu)
|
||||
|
||||
if (showMenu) {
|
||||
if (newValue.startsWith("/")) {
|
||||
if (newValue.startsWith("/") && !newValue.includes(" ")) {
|
||||
// Handle slash command - request fresh commands
|
||||
const query = newValue
|
||||
setSearchQuery(query)
|
||||
|
|
@ -716,6 +718,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
.replace(/\n$/, "\n\n")
|
||||
.replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c)
|
||||
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
|
||||
.replace(commandRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
|
||||
|
||||
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
|
||||
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ interface ContextMenuProps {
|
|||
const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
onSelect,
|
||||
searchQuery,
|
||||
inputValue,
|
||||
onMouseDown,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
|
|
@ -44,16 +43,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
return getContextMenuOptions(
|
||||
searchQuery,
|
||||
inputValue,
|
||||
selectedType,
|
||||
queryItems,
|
||||
dynamicSearchResults,
|
||||
modes,
|
||||
commands,
|
||||
)
|
||||
}, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes, commands])
|
||||
return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands)
|
||||
}, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands])
|
||||
|
||||
useEffect(() => {
|
||||
if (menuRef.current) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe("insertMention", () => {
|
|||
})
|
||||
|
||||
it("should handle slash command replacement", () => {
|
||||
const result = insertMention("/mode some", 5, "code") // Simulating mode selection
|
||||
const result = insertMention("/mode some", 5, "code", true) // Simulating mode selection
|
||||
expect(result.newValue).toBe("code") // Should replace the whole text
|
||||
expect(result.mentionIndex).toBe(0)
|
||||
})
|
||||
|
|
@ -103,6 +103,48 @@ describe("insertMention", () => {
|
|||
expect(result.mentionIndex).toBe(7)
|
||||
expect(result.newValue.includes("\\ ")).toBe(false)
|
||||
})
|
||||
|
||||
// --- Tests for isSlashCommand parameter ---
|
||||
describe("isSlashCommand parameter", () => {
|
||||
it("should replace entire text when isSlashCommand is true", () => {
|
||||
const result = insertMention("/cod", 4, "code", true)
|
||||
expect(result.newValue).toBe("code")
|
||||
expect(result.mentionIndex).toBe(0)
|
||||
})
|
||||
|
||||
it("should replace entire text even when @ mentions exist and isSlashCommand is true", () => {
|
||||
const result = insertMention("/code @some/file.ts", 5, "debug", true)
|
||||
expect(result.newValue).toBe("debug")
|
||||
expect(result.mentionIndex).toBe(0)
|
||||
})
|
||||
|
||||
it("should insert @ mention correctly after slash command when isSlashCommand is false", () => {
|
||||
const text = "/code @"
|
||||
const position = 8 // cursor after @
|
||||
const result = insertMention(text, position, "src/file.ts", false)
|
||||
|
||||
expect(result.newValue).toBe("/code @src/file.ts ")
|
||||
expect(result.mentionIndex).toBe(6) // position of @
|
||||
})
|
||||
|
||||
it("should not treat text starting with / as slash command when isSlashCommand is false", () => {
|
||||
const text = "/some/path/file.ts @"
|
||||
const position = 20
|
||||
const result = insertMention(text, position, "another.ts", false)
|
||||
|
||||
expect(result.newValue).toBe("/some/path/file.ts @another.ts ")
|
||||
expect(result.mentionIndex).toBe(19) // position of @
|
||||
})
|
||||
|
||||
it("should work with default parameter (isSlashCommand = false)", () => {
|
||||
const text = "/code @"
|
||||
const position = 8
|
||||
const result = insertMention(text, position, "src/file.ts") // No isSlashCommand parameter
|
||||
|
||||
expect(result.newValue).toBe("/code @src/file.ts ")
|
||||
expect(result.mentionIndex).toBe(6)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeMention", () => {
|
||||
|
|
@ -195,7 +237,7 @@ describe("getContextMenuOptions", () => {
|
|||
]
|
||||
|
||||
it("should return all option types for empty query", () => {
|
||||
const result = getContextMenuOptions("", "", null, [])
|
||||
const result = getContextMenuOptions("", null, [])
|
||||
expect(result).toHaveLength(6)
|
||||
expect(result.map((item) => item.type)).toEqual([
|
||||
ContextMenuOptionType.Problems,
|
||||
|
|
@ -208,7 +250,7 @@ describe("getContextMenuOptions", () => {
|
|||
})
|
||||
|
||||
it("should filter by selected type when query is empty", () => {
|
||||
const result = getContextMenuOptions("", "", ContextMenuOptionType.File, mockQueryItems)
|
||||
const result = getContextMenuOptions("", ContextMenuOptionType.File, mockQueryItems)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.File)
|
||||
expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.OpenedFile)
|
||||
|
|
@ -217,19 +259,19 @@ describe("getContextMenuOptions", () => {
|
|||
})
|
||||
|
||||
it("should match git commands", () => {
|
||||
const result = getContextMenuOptions("git", "git", null, mockQueryItems)
|
||||
const result = getContextMenuOptions("git", null, mockQueryItems)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.Git)
|
||||
expect(result[0].label).toBe("Git Commits")
|
||||
})
|
||||
|
||||
it("should match git commit hashes", () => {
|
||||
const result = getContextMenuOptions("abc1234", "abc1234", null, mockQueryItems)
|
||||
const result = getContextMenuOptions("abc1234", null, mockQueryItems)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.Git)
|
||||
expect(result[0].value).toBe("abc1234")
|
||||
})
|
||||
|
||||
it("should return NoResults when no matches found", () => {
|
||||
const result = getContextMenuOptions("nonexistent", "nonexistent", null, mockQueryItems)
|
||||
const result = getContextMenuOptions("nonexistent", null, mockQueryItems)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.NoResults)
|
||||
})
|
||||
|
|
@ -250,7 +292,7 @@ describe("getContextMenuOptions", () => {
|
|||
},
|
||||
]
|
||||
|
||||
const result = getContextMenuOptions("test", "test", null, testItems, mockDynamicSearchResults)
|
||||
const result = getContextMenuOptions("test", null, testItems, mockDynamicSearchResults)
|
||||
|
||||
// Check if opened files and dynamic search results are included
|
||||
expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true)
|
||||
|
|
@ -259,7 +301,7 @@ describe("getContextMenuOptions", () => {
|
|||
|
||||
it("should maintain correct result ordering according to implementation", () => {
|
||||
// Add multiple item types to test ordering
|
||||
const result = getContextMenuOptions("t", "t", null, mockQueryItems, mockDynamicSearchResults)
|
||||
const result = getContextMenuOptions("t", null, mockQueryItems, mockDynamicSearchResults)
|
||||
|
||||
// Find the different result types
|
||||
const fileResults = result.filter(
|
||||
|
|
@ -290,7 +332,7 @@ describe("getContextMenuOptions", () => {
|
|||
})
|
||||
|
||||
it("should include opened files when dynamic search results exist", () => {
|
||||
const result = getContextMenuOptions("open", "open", null, mockQueryItems, mockDynamicSearchResults)
|
||||
const result = getContextMenuOptions("open", null, mockQueryItems, mockDynamicSearchResults)
|
||||
|
||||
// Verify opened files are included
|
||||
expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true)
|
||||
|
|
@ -299,7 +341,7 @@ describe("getContextMenuOptions", () => {
|
|||
})
|
||||
|
||||
it("should include git results when dynamic search results exist", () => {
|
||||
const result = getContextMenuOptions("commit", "commit", null, mockQueryItems, mockDynamicSearchResults)
|
||||
const result = getContextMenuOptions("commit", null, mockQueryItems, mockDynamicSearchResults)
|
||||
|
||||
// Verify git results are included
|
||||
expect(result.some((item) => item.type === ContextMenuOptionType.Git)).toBe(true)
|
||||
|
|
@ -320,7 +362,7 @@ describe("getContextMenuOptions", () => {
|
|||
},
|
||||
]
|
||||
|
||||
const result = getContextMenuOptions("test", "test", null, mockQueryItems, duplicateSearchResults)
|
||||
const result = getContextMenuOptions("test", null, mockQueryItems, duplicateSearchResults)
|
||||
|
||||
// Count occurrences of src/test.ts in results
|
||||
const duplicateCount = result.filter(
|
||||
|
|
@ -338,7 +380,6 @@ describe("getContextMenuOptions", () => {
|
|||
it("should return NoResults when all combined results are empty with dynamic search", () => {
|
||||
// Use a query that won't match anything
|
||||
const result = getContextMenuOptions(
|
||||
"nonexistentquery123456",
|
||||
"nonexistentquery123456",
|
||||
null,
|
||||
mockQueryItems,
|
||||
|
|
@ -387,7 +428,7 @@ describe("getContextMenuOptions", () => {
|
|||
]
|
||||
|
||||
// Get results for "test" query
|
||||
const result = getContextMenuOptions(testQuery, testQuery, null, testItems, testSearchResults)
|
||||
const result = getContextMenuOptions(testQuery, null, testItems, testSearchResults)
|
||||
|
||||
// Verify we have results
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
|
|
@ -433,7 +474,7 @@ describe("getContextMenuOptions", () => {
|
|||
},
|
||||
]
|
||||
|
||||
const result = getContextMenuOptions("/co", "/co", null, [], [], mockModes)
|
||||
const result = getContextMenuOptions("/co", null, [], [], mockModes)
|
||||
|
||||
// Should have section header first, then mode results
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.SectionHeader)
|
||||
|
|
@ -444,7 +485,7 @@ describe("getContextMenuOptions", () => {
|
|||
it("should not process slash commands when query starts with slash but inputValue doesn't", () => {
|
||||
// Use a completely non-matching query to ensure we get NoResults
|
||||
// and provide empty query items to avoid any matches
|
||||
const result = getContextMenuOptions("/nonexistentquery", "Hello /code", null, [], [])
|
||||
const result = getContextMenuOptions("/nonexistentquery", null, [], [])
|
||||
|
||||
// Should not process as a mode command
|
||||
expect(result[0].type).not.toBe(ContextMenuOptionType.Mode)
|
||||
|
|
@ -454,7 +495,7 @@ describe("getContextMenuOptions", () => {
|
|||
|
||||
// --- Tests for Escaped Spaces (Focus on how paths are presented) ---
|
||||
it("should return search results with correct labels/descriptions (no escaping needed here)", () => {
|
||||
const options = getContextMenuOptions("@search", "search", null, mockQueryItems, mockSearchResults)
|
||||
const options = getContextMenuOptions("@search", null, mockQueryItems, mockSearchResults)
|
||||
const fileResult = options.find((o) => o.label === "search result spaces.ts")
|
||||
expect(fileResult).toBeDefined()
|
||||
// Value should be the normalized path, description might be the same or label
|
||||
|
|
@ -467,7 +508,7 @@ describe("getContextMenuOptions", () => {
|
|||
})
|
||||
|
||||
it("should return query items (like opened files) with correct labels/descriptions", () => {
|
||||
const options = getContextMenuOptions("open", "@open", null, mockQueryItems, [])
|
||||
const options = getContextMenuOptions("open", null, mockQueryItems, [])
|
||||
const openedFile = options.find((o) => o.label === "open file.ts")
|
||||
expect(openedFile).toBeDefined()
|
||||
expect(openedFile?.value).toBe("src/open file.ts")
|
||||
|
|
@ -484,7 +525,7 @@ describe("getContextMenuOptions", () => {
|
|||
]
|
||||
|
||||
// The formatting happens in getContextMenuOptions when converting search results to menu items
|
||||
const formattedItems = getContextMenuOptions("spaces", "@spaces", null, [], searchResults)
|
||||
const formattedItems = getContextMenuOptions("spaces", null, [], searchResults)
|
||||
|
||||
// Verify we get some results back that aren't "No Results"
|
||||
expect(formattedItems.length).toBeGreaterThan(0)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ export function insertMention(
|
|||
text: string,
|
||||
position: number,
|
||||
value: string,
|
||||
isSlashCommand: boolean = false,
|
||||
): { newValue: string; mentionIndex: number } {
|
||||
// Handle slash command
|
||||
if (text.startsWith("/")) {
|
||||
// Handle slash command selection (only when explicitly selecting a slash command)
|
||||
if (isSlashCommand) {
|
||||
return {
|
||||
newValue: value,
|
||||
mentionIndex: 0,
|
||||
|
|
@ -122,7 +123,6 @@ export interface ContextMenuQueryItem {
|
|||
|
||||
export function getContextMenuOptions(
|
||||
query: string,
|
||||
inputValue: string,
|
||||
selectedType: ContextMenuOptionType | null = null,
|
||||
queryItems: ContextMenuQueryItem[],
|
||||
dynamicSearchResults: SearchResult[] = [],
|
||||
|
|
@ -130,7 +130,8 @@ export function getContextMenuOptions(
|
|||
commands?: Command[],
|
||||
): ContextMenuQueryItem[] {
|
||||
// Handle slash commands for modes and commands
|
||||
if (query.startsWith("/") && inputValue.startsWith("/")) {
|
||||
// Only process as slash command if the query itself starts with "/" (meaning we're typing a slash command)
|
||||
if (query.startsWith("/")) {
|
||||
const slashQuery = query.slice(1)
|
||||
const results: ContextMenuQueryItem[] = []
|
||||
|
||||
|
|
@ -362,11 +363,14 @@ export function getContextMenuOptions(
|
|||
}
|
||||
|
||||
export function shouldShowContextMenu(text: string, position: number): boolean {
|
||||
// Handle slash command
|
||||
if (text.startsWith("/")) {
|
||||
return position <= text.length && !text.includes(" ")
|
||||
}
|
||||
const beforeCursor = text.slice(0, position)
|
||||
|
||||
// Check if we're in a slash command context (at the beginning and no space yet)
|
||||
if (text.startsWith("/") && !text.includes(" ") && position <= text.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for @ mention context
|
||||
const atIndex = beforeCursor.lastIndexOf("@")
|
||||
|
||||
if (atIndex === -1) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue