import { mentionRegex } from "../../../src/shared/context-mentions" import { Fzf } from "fzf" export function insertMention( text: string, position: number, value: string, ): { newValue: string; mentionIndex: number } { const beforeCursor = text.slice(0, position) const afterCursor = text.slice(position) // Find the position of the last '@' symbol before the cursor const lastAtIndex = beforeCursor.lastIndexOf("@") let newValue: string let mentionIndex: number if (lastAtIndex !== -1) { // If there's an '@' symbol, replace everything after it with the new mention const beforeMention = text.slice(0, lastAtIndex) newValue = beforeMention + "@" + value + " " + afterCursor.replace(/^[^\s]*/, "") mentionIndex = lastAtIndex } else { // If there's no '@' symbol, insert the mention at the cursor position newValue = beforeCursor + "@" + value + " " + afterCursor mentionIndex = position } return { newValue, mentionIndex } } export function removeMention(text: string, position: number): { newText: string; newPosition: number } { const beforeCursor = text.slice(0, position) const afterCursor = text.slice(position) // Check if we're at the end of a mention const matchEnd = beforeCursor.match(new RegExp(mentionRegex.source + "$")) if (matchEnd) { // If we're at the end of a mention, remove it const newText = text.slice(0, position - matchEnd[0].length) + afterCursor.replace(" ", "") // removes the first space after the mention const newPosition = position - matchEnd[0].length return { newText, newPosition } } // If we're not at the end of a mention, just return the original text and position return { newText: text, newPosition: position } } export enum ContextMenuOptionType { OpenedFile = "openedFile", File = "file", Folder = "folder", Problems = "problems", URL = "url", Git = "git", NoResults = "noResults", } export interface ContextMenuQueryItem { type: ContextMenuOptionType value?: string label?: string description?: string icon?: string } export function getContextMenuOptions( query: string, selectedType: ContextMenuOptionType | null = null, queryItems: ContextMenuQueryItem[], ): ContextMenuQueryItem[] { const workingChanges: ContextMenuQueryItem = { type: ContextMenuOptionType.Git, value: "git-changes", label: "Working changes", description: "Current uncommitted changes", icon: "$(git-commit)", } if (query === "") { if (selectedType === ContextMenuOptionType.File) { const files = queryItems .filter( (item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.OpenedFile, ) .map((item) => ({ type: item.type, value: item.value, })) return files.length > 0 ? files : [{ type: ContextMenuOptionType.NoResults }] } if (selectedType === ContextMenuOptionType.Folder) { const folders = queryItems .filter((item) => item.type === ContextMenuOptionType.Folder) .map((item) => ({ type: ContextMenuOptionType.Folder, value: item.value })) return folders.length > 0 ? folders : [{ type: ContextMenuOptionType.NoResults }] } if (selectedType === ContextMenuOptionType.Git) { const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git) return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges] } return [ { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.URL }, { type: ContextMenuOptionType.Folder }, { type: ContextMenuOptionType.File }, { type: ContextMenuOptionType.Git }, ] } const lowerQuery = query.toLowerCase() const suggestions: ContextMenuQueryItem[] = [] // Check for top-level option matches if ("git".startsWith(lowerQuery)) { suggestions.push({ type: ContextMenuOptionType.Git, label: "Git Commits", description: "Search repository history", icon: "$(git-commit)", }) } else if ("git-changes".startsWith(lowerQuery)) { suggestions.push(workingChanges) } if ("problems".startsWith(lowerQuery)) { suggestions.push({ type: ContextMenuOptionType.Problems }) } if (query.startsWith("http")) { suggestions.push({ type: ContextMenuOptionType.URL, value: query }) } // Add exact SHA matches to suggestions if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) { const exactMatches = queryItems.filter( (item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery, ) if (exactMatches.length > 0) { suggestions.push(...exactMatches) } else { // If no exact match but valid SHA format, add as option suggestions.push({ type: ContextMenuOptionType.Git, value: lowerQuery, label: `Commit ${lowerQuery}`, description: "Git commit hash", icon: "$(git-commit)", }) } } // Create searchable strings array for fzf const searchableItems = queryItems.map((item) => ({ original: item, searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "), })) // Initialize fzf instance for fuzzy search const fzf = new Fzf(searchableItems, { selector: (item) => item.searchStr, }) // Get fuzzy matching items const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : [] // Separate matches by type const fileMatches = matchingItems.filter( (item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.OpenedFile || item.type === ContextMenuOptionType.Folder, ) const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git) const otherMatches = matchingItems.filter( (item) => item.type !== ContextMenuOptionType.File && item.type !== ContextMenuOptionType.OpenedFile && item.type !== ContextMenuOptionType.Folder && item.type !== ContextMenuOptionType.Git, ) // Combine suggestions with matching items in the desired order if (suggestions.length > 0 || matchingItems.length > 0) { const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches] // Remove duplicates based on type and value const seen = new Set() const deduped = allItems.filter((item) => { const key = `${item.type}-${item.value}` if (seen.has(key)) return false seen.add(key) return true }) return deduped } return [{ type: ContextMenuOptionType.NoResults }] } export function shouldShowContextMenu(text: string, position: number): boolean { const beforeCursor = text.slice(0, position) const atIndex = beforeCursor.lastIndexOf("@") if (atIndex === -1) return false const textAfterAt = beforeCursor.slice(atIndex + 1) // Check if there's any whitespace after the '@' if (/\s/.test(textAfterAt)) return false // Don't show the menu if it's a URL if (textAfterAt.toLowerCase().startsWith("http")) return false // Don't show the menu if it's a problems if (textAfterAt.toLowerCase().startsWith("problems")) return false // NOTE: it's okay that menu shows when there's trailing punctuation since user could be inputting a path with marks // Show the menu if there's just '@' or '@' followed by some text (but not a URL) return true }