mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 21:01:06 -05:00
Prettier backfill
This commit is contained in:
@@ -1,101 +1,110 @@
|
||||
import { parseCommand, isAllowedSingleCommand, validateCommand } from '../command-validation'
|
||||
import { parseCommand, isAllowedSingleCommand, validateCommand } from "../command-validation"
|
||||
|
||||
describe('Command Validation', () => {
|
||||
describe('parseCommand', () => {
|
||||
it('splits commands by chain operators', () => {
|
||||
expect(parseCommand('npm test && npm run build')).toEqual(['npm test', 'npm run build'])
|
||||
expect(parseCommand('npm test || npm run build')).toEqual(['npm test', 'npm run build'])
|
||||
expect(parseCommand('npm test; npm run build')).toEqual(['npm test', 'npm run build'])
|
||||
expect(parseCommand('npm test | npm run build')).toEqual(['npm test', 'npm run build'])
|
||||
describe("Command Validation", () => {
|
||||
describe("parseCommand", () => {
|
||||
it("splits commands by chain operators", () => {
|
||||
expect(parseCommand("npm test && npm run build")).toEqual(["npm test", "npm run build"])
|
||||
expect(parseCommand("npm test || npm run build")).toEqual(["npm test", "npm run build"])
|
||||
expect(parseCommand("npm test; npm run build")).toEqual(["npm test", "npm run build"])
|
||||
expect(parseCommand("npm test | npm run build")).toEqual(["npm test", "npm run build"])
|
||||
})
|
||||
|
||||
it('preserves quoted content', () => {
|
||||
it("preserves quoted content", () => {
|
||||
expect(parseCommand('npm test "param with | inside"')).toEqual(['npm test "param with | inside"'])
|
||||
expect(parseCommand('echo "hello | world"')).toEqual(['echo "hello | world"'])
|
||||
expect(parseCommand('npm test "param with && inside"')).toEqual(['npm test "param with && inside"'])
|
||||
})
|
||||
|
||||
it('handles subshell patterns', () => {
|
||||
expect(parseCommand('npm test $(echo test)')).toEqual(['npm test', 'echo test'])
|
||||
expect(parseCommand('npm test `echo test`')).toEqual(['npm test', 'echo test'])
|
||||
it("handles subshell patterns", () => {
|
||||
expect(parseCommand("npm test $(echo test)")).toEqual(["npm test", "echo test"])
|
||||
expect(parseCommand("npm test `echo test`")).toEqual(["npm test", "echo test"])
|
||||
})
|
||||
|
||||
it('handles empty and whitespace input', () => {
|
||||
expect(parseCommand('')).toEqual([])
|
||||
expect(parseCommand(' ')).toEqual([])
|
||||
expect(parseCommand('\t')).toEqual([])
|
||||
it("handles empty and whitespace input", () => {
|
||||
expect(parseCommand("")).toEqual([])
|
||||
expect(parseCommand(" ")).toEqual([])
|
||||
expect(parseCommand("\t")).toEqual([])
|
||||
})
|
||||
|
||||
it('handles PowerShell specific patterns', () => {
|
||||
expect(parseCommand('npm test 2>&1 | Select-String "Error"')).toEqual(['npm test 2>&1', 'Select-String "Error"'])
|
||||
expect(parseCommand('npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'))
|
||||
.toEqual(['npm test', 'Select-String -NotMatch "node_modules"', 'Select-String "FAIL|Error"'])
|
||||
it("handles PowerShell specific patterns", () => {
|
||||
expect(parseCommand('npm test 2>&1 | Select-String "Error"')).toEqual([
|
||||
"npm test 2>&1",
|
||||
'Select-String "Error"',
|
||||
])
|
||||
expect(
|
||||
parseCommand('npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'),
|
||||
).toEqual(["npm test", 'Select-String -NotMatch "node_modules"', 'Select-String "FAIL|Error"'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAllowedSingleCommand', () => {
|
||||
const allowedCommands = ['npm test', 'npm run', 'echo']
|
||||
describe("isAllowedSingleCommand", () => {
|
||||
const allowedCommands = ["npm test", "npm run", "echo"]
|
||||
|
||||
it('matches commands case-insensitively', () => {
|
||||
expect(isAllowedSingleCommand('NPM TEST', allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand('npm TEST --coverage', allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand('ECHO hello', allowedCommands)).toBe(true)
|
||||
it("matches commands case-insensitively", () => {
|
||||
expect(isAllowedSingleCommand("NPM TEST", allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand("npm TEST --coverage", allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand("ECHO hello", allowedCommands)).toBe(true)
|
||||
})
|
||||
|
||||
it('matches command prefixes', () => {
|
||||
expect(isAllowedSingleCommand('npm test --coverage', allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand('npm run build', allowedCommands)).toBe(true)
|
||||
it("matches command prefixes", () => {
|
||||
expect(isAllowedSingleCommand("npm test --coverage", allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand("npm run build", allowedCommands)).toBe(true)
|
||||
expect(isAllowedSingleCommand('echo "hello world"', allowedCommands)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects non-matching commands', () => {
|
||||
expect(isAllowedSingleCommand('npmtest', allowedCommands)).toBe(false)
|
||||
expect(isAllowedSingleCommand('dangerous', allowedCommands)).toBe(false)
|
||||
expect(isAllowedSingleCommand('rm -rf /', allowedCommands)).toBe(false)
|
||||
it("rejects non-matching commands", () => {
|
||||
expect(isAllowedSingleCommand("npmtest", allowedCommands)).toBe(false)
|
||||
expect(isAllowedSingleCommand("dangerous", allowedCommands)).toBe(false)
|
||||
expect(isAllowedSingleCommand("rm -rf /", allowedCommands)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles undefined/empty allowed commands', () => {
|
||||
expect(isAllowedSingleCommand('npm test', undefined as any)).toBe(false)
|
||||
expect(isAllowedSingleCommand('npm test', [])).toBe(false)
|
||||
it("handles undefined/empty allowed commands", () => {
|
||||
expect(isAllowedSingleCommand("npm test", undefined as any)).toBe(false)
|
||||
expect(isAllowedSingleCommand("npm test", [])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateCommand', () => {
|
||||
const allowedCommands = ['npm test', 'npm run', 'echo', 'Select-String']
|
||||
describe("validateCommand", () => {
|
||||
const allowedCommands = ["npm test", "npm run", "echo", "Select-String"]
|
||||
|
||||
it('validates simple commands', () => {
|
||||
expect(validateCommand('npm test', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm run build', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('dangerous', allowedCommands)).toBe(false)
|
||||
it("validates simple commands", () => {
|
||||
expect(validateCommand("npm test", allowedCommands)).toBe(true)
|
||||
expect(validateCommand("npm run build", allowedCommands)).toBe(true)
|
||||
expect(validateCommand("dangerous", allowedCommands)).toBe(false)
|
||||
})
|
||||
|
||||
it('validates chained commands', () => {
|
||||
expect(validateCommand('npm test && npm run build', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm test && dangerous', allowedCommands)).toBe(false)
|
||||
it("validates chained commands", () => {
|
||||
expect(validateCommand("npm test && npm run build", allowedCommands)).toBe(true)
|
||||
expect(validateCommand("npm test && dangerous", allowedCommands)).toBe(false)
|
||||
expect(validateCommand('npm test | Select-String "Error"', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm test | rm -rf /', allowedCommands)).toBe(false)
|
||||
expect(validateCommand("npm test | rm -rf /", allowedCommands)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles quoted content correctly', () => {
|
||||
it("handles quoted content correctly", () => {
|
||||
expect(validateCommand('npm test "param with | inside"', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('echo "hello | world"', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm test "param with && inside"', allowedCommands)).toBe(true)
|
||||
})
|
||||
|
||||
it('handles subshell execution attempts', () => {
|
||||
expect(validateCommand('npm test $(echo dangerous)', allowedCommands)).toBe(false)
|
||||
expect(validateCommand('npm test `rm -rf /`', allowedCommands)).toBe(false)
|
||||
it("handles subshell execution attempts", () => {
|
||||
expect(validateCommand("npm test $(echo dangerous)", allowedCommands)).toBe(false)
|
||||
expect(validateCommand("npm test `rm -rf /`", allowedCommands)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles PowerShell patterns', () => {
|
||||
it("handles PowerShell patterns", () => {
|
||||
expect(validateCommand('npm test 2>&1 | Select-String "Error"', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"', allowedCommands)).toBe(true)
|
||||
expect(validateCommand('npm test | Select-String | dangerous', allowedCommands)).toBe(false)
|
||||
expect(
|
||||
validateCommand(
|
||||
'npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"',
|
||||
allowedCommands,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(validateCommand("npm test | Select-String | dangerous", allowedCommands)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles empty input', () => {
|
||||
expect(validateCommand('', allowedCommands)).toBe(true)
|
||||
expect(validateCommand(' ', allowedCommands)).toBe(true)
|
||||
it("handles empty input", () => {
|
||||
expect(validateCommand("", allowedCommands)).toBe(true)
|
||||
expect(validateCommand(" ", allowedCommands)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,130 +1,137 @@
|
||||
import { insertMention, removeMention, getContextMenuOptions, shouldShowContextMenu, ContextMenuOptionType, ContextMenuQueryItem } from '../context-mentions'
|
||||
import {
|
||||
insertMention,
|
||||
removeMention,
|
||||
getContextMenuOptions,
|
||||
shouldShowContextMenu,
|
||||
ContextMenuOptionType,
|
||||
ContextMenuQueryItem,
|
||||
} from "../context-mentions"
|
||||
|
||||
describe('insertMention', () => {
|
||||
it('should insert mention at cursor position when no @ symbol exists', () => {
|
||||
const result = insertMention('Hello world', 5, 'test')
|
||||
expect(result.newValue).toBe('Hello@test world')
|
||||
describe("insertMention", () => {
|
||||
it("should insert mention at cursor position when no @ symbol exists", () => {
|
||||
const result = insertMention("Hello world", 5, "test")
|
||||
expect(result.newValue).toBe("Hello@test world")
|
||||
expect(result.mentionIndex).toBe(5)
|
||||
})
|
||||
|
||||
it('should replace text after last @ symbol', () => {
|
||||
const result = insertMention('Hello @wor world', 8, 'test')
|
||||
expect(result.newValue).toBe('Hello @test world')
|
||||
it("should replace text after last @ symbol", () => {
|
||||
const result = insertMention("Hello @wor world", 8, "test")
|
||||
expect(result.newValue).toBe("Hello @test world")
|
||||
expect(result.mentionIndex).toBe(6)
|
||||
})
|
||||
|
||||
it('should handle empty text', () => {
|
||||
const result = insertMention('', 0, 'test')
|
||||
expect(result.newValue).toBe('@test ')
|
||||
it("should handle empty text", () => {
|
||||
const result = insertMention("", 0, "test")
|
||||
expect(result.newValue).toBe("@test ")
|
||||
expect(result.mentionIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeMention', () => {
|
||||
it('should remove mention when cursor is at end of mention', () => {
|
||||
describe("removeMention", () => {
|
||||
it("should remove mention when cursor is at end of mention", () => {
|
||||
// Test with the problems keyword that matches the regex
|
||||
const result = removeMention('Hello @problems ', 15)
|
||||
expect(result.newText).toBe('Hello ')
|
||||
const result = removeMention("Hello @problems ", 15)
|
||||
expect(result.newText).toBe("Hello ")
|
||||
expect(result.newPosition).toBe(6)
|
||||
})
|
||||
|
||||
it('should not remove text when not at end of mention', () => {
|
||||
const result = removeMention('Hello @test world', 8)
|
||||
expect(result.newText).toBe('Hello @test world')
|
||||
it("should not remove text when not at end of mention", () => {
|
||||
const result = removeMention("Hello @test world", 8)
|
||||
expect(result.newText).toBe("Hello @test world")
|
||||
expect(result.newPosition).toBe(8)
|
||||
})
|
||||
|
||||
it('should handle text without mentions', () => {
|
||||
const result = removeMention('Hello world', 5)
|
||||
expect(result.newText).toBe('Hello world')
|
||||
it("should handle text without mentions", () => {
|
||||
const result = removeMention("Hello world", 5)
|
||||
expect(result.newText).toBe("Hello world")
|
||||
expect(result.newPosition).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getContextMenuOptions', () => {
|
||||
describe("getContextMenuOptions", () => {
|
||||
const mockQueryItems: ContextMenuQueryItem[] = [
|
||||
{
|
||||
type: ContextMenuOptionType.File,
|
||||
value: 'src/test.ts',
|
||||
label: 'test.ts',
|
||||
description: 'Source file'
|
||||
value: "src/test.ts",
|
||||
label: "test.ts",
|
||||
description: "Source file",
|
||||
},
|
||||
{
|
||||
type: ContextMenuOptionType.Git,
|
||||
value: 'abc1234',
|
||||
label: 'Initial commit',
|
||||
description: 'First commit',
|
||||
icon: '$(git-commit)'
|
||||
value: "abc1234",
|
||||
label: "Initial commit",
|
||||
description: "First commit",
|
||||
icon: "$(git-commit)",
|
||||
},
|
||||
{
|
||||
type: ContextMenuOptionType.Folder,
|
||||
value: 'src',
|
||||
label: 'src',
|
||||
description: 'Source folder'
|
||||
}
|
||||
value: "src",
|
||||
label: "src",
|
||||
description: "Source folder",
|
||||
},
|
||||
]
|
||||
|
||||
it('should return all option types for empty query', () => {
|
||||
const result = getContextMenuOptions('', null, [])
|
||||
it("should return all option types for empty query", () => {
|
||||
const result = getContextMenuOptions("", null, [])
|
||||
expect(result).toHaveLength(5)
|
||||
expect(result.map(item => item.type)).toEqual([
|
||||
expect(result.map((item) => item.type)).toEqual([
|
||||
ContextMenuOptionType.Problems,
|
||||
ContextMenuOptionType.URL,
|
||||
ContextMenuOptionType.Folder,
|
||||
ContextMenuOptionType.File,
|
||||
ContextMenuOptionType.Git
|
||||
ContextMenuOptionType.Git,
|
||||
])
|
||||
})
|
||||
|
||||
it('should filter by selected type when query is empty', () => {
|
||||
const result = getContextMenuOptions('', ContextMenuOptionType.File, mockQueryItems)
|
||||
it("should filter by selected type when query is empty", () => {
|
||||
const result = getContextMenuOptions("", ContextMenuOptionType.File, mockQueryItems)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.File)
|
||||
expect(result[0].value).toBe('src/test.ts')
|
||||
expect(result[0].value).toBe("src/test.ts")
|
||||
})
|
||||
|
||||
it('should match git commands', () => {
|
||||
const result = getContextMenuOptions('git', null, mockQueryItems)
|
||||
it("should match git commands", () => {
|
||||
const result = getContextMenuOptions("git", null, mockQueryItems)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.Git)
|
||||
expect(result[0].label).toBe('Git Commits')
|
||||
expect(result[0].label).toBe("Git Commits")
|
||||
})
|
||||
|
||||
it('should match git commit hashes', () => {
|
||||
const result = getContextMenuOptions('abc1234', null, mockQueryItems)
|
||||
it("should match git commit hashes", () => {
|
||||
const result = getContextMenuOptions("abc1234", null, mockQueryItems)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.Git)
|
||||
expect(result[0].value).toBe('abc1234')
|
||||
expect(result[0].value).toBe("abc1234")
|
||||
})
|
||||
|
||||
it('should return NoResults when no matches found', () => {
|
||||
const result = getContextMenuOptions('nonexistent', null, mockQueryItems)
|
||||
it("should return NoResults when no matches found", () => {
|
||||
const result = getContextMenuOptions("nonexistent", null, mockQueryItems)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe(ContextMenuOptionType.NoResults)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowContextMenu', () => {
|
||||
it('should return true for @ symbol', () => {
|
||||
expect(shouldShowContextMenu('@', 1)).toBe(true)
|
||||
describe("shouldShowContextMenu", () => {
|
||||
it("should return true for @ symbol", () => {
|
||||
expect(shouldShowContextMenu("@", 1)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for @ followed by text', () => {
|
||||
expect(shouldShowContextMenu('Hello @test', 10)).toBe(true)
|
||||
it("should return true for @ followed by text", () => {
|
||||
expect(shouldShowContextMenu("Hello @test", 10)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when no @ symbol exists', () => {
|
||||
expect(shouldShowContextMenu('Hello world', 5)).toBe(false)
|
||||
it("should return false when no @ symbol exists", () => {
|
||||
expect(shouldShowContextMenu("Hello world", 5)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for @ followed by whitespace', () => {
|
||||
expect(shouldShowContextMenu('Hello @ world', 6)).toBe(false)
|
||||
it("should return false for @ followed by whitespace", () => {
|
||||
expect(shouldShowContextMenu("Hello @ world", 6)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for @ in URL', () => {
|
||||
expect(shouldShowContextMenu('Hello @http://test.com', 17)).toBe(false)
|
||||
it("should return false for @ in URL", () => {
|
||||
expect(shouldShowContextMenu("Hello @http://test.com", 17)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for @problems', () => {
|
||||
it("should return false for @problems", () => {
|
||||
// Position cursor at the end to test the full word
|
||||
expect(shouldShowContextMenu('@problems', 9)).toBe(false)
|
||||
expect(shouldShowContextMenu("@problems", 9)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parse } from 'shell-quote'
|
||||
import { parse } from "shell-quote"
|
||||
|
||||
type ShellToken = string | { op: string } | { command: string }
|
||||
|
||||
@@ -46,39 +46,39 @@ export function parseCommand(command: string): string[] {
|
||||
let currentCommand: string[] = []
|
||||
|
||||
for (const token of tokens) {
|
||||
if (typeof token === 'object' && 'op' in token) {
|
||||
if (typeof token === "object" && "op" in token) {
|
||||
// Chain operator - split command
|
||||
if (['&&', '||', ';', '|'].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(' '))
|
||||
currentCommand = []
|
||||
}
|
||||
if (["&&", "||", ";", "|"].includes(token.op)) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
} else {
|
||||
// Other operators (>, &) are part of the command
|
||||
currentCommand.push(token.op)
|
||||
// Other operators (>, &) are part of the command
|
||||
currentCommand.push(token.op)
|
||||
}
|
||||
} else if (typeof token === 'string') {
|
||||
} else if (typeof token === "string") {
|
||||
// Check if it's a subshell placeholder
|
||||
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
|
||||
if (subshellMatch) {
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(' '))
|
||||
currentCommand = []
|
||||
}
|
||||
commands.push(subshells[parseInt(subshellMatch[1])])
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(" "))
|
||||
currentCommand = []
|
||||
}
|
||||
commands.push(subshells[parseInt(subshellMatch[1])])
|
||||
} else {
|
||||
currentCommand.push(token)
|
||||
currentCommand.push(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining command
|
||||
if (currentCommand.length > 0) {
|
||||
commands.push(currentCommand.join(' '))
|
||||
commands.push(currentCommand.join(" "))
|
||||
}
|
||||
|
||||
// Restore quotes and redirections
|
||||
return commands.map(cmd => {
|
||||
return commands.map((cmd) => {
|
||||
let result = cmd
|
||||
// Restore quotes
|
||||
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
|
||||
@@ -91,15 +91,10 @@ export function parseCommand(command: string): string[] {
|
||||
/**
|
||||
* Check if a single command is allowed based on prefix matching.
|
||||
*/
|
||||
export function isAllowedSingleCommand(
|
||||
command: string,
|
||||
allowedCommands: string[]
|
||||
): boolean {
|
||||
export function isAllowedSingleCommand(command: string, allowedCommands: string[]): boolean {
|
||||
if (!command || !allowedCommands?.length) return false
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
return allowedCommands.some(prefix =>
|
||||
trimmedCommand.startsWith(prefix.toLowerCase())
|
||||
)
|
||||
return allowedCommands.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +105,7 @@ export function validateCommand(command: string, allowedCommands: string[]): boo
|
||||
if (!command?.trim()) return true
|
||||
|
||||
// Block subshell execution attempts
|
||||
if (command.includes('$(') || command.includes('`')) {
|
||||
if (command.includes("$(") || command.includes("`")) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -118,9 +113,9 @@ export function validateCommand(command: string, allowedCommands: string[]): boo
|
||||
const subCommands = parseCommand(command)
|
||||
|
||||
// Then ensure every sub-command starts with an allowed prefix
|
||||
return subCommands.every(cmd => {
|
||||
return subCommands.every((cmd) => {
|
||||
// Remove simple PowerShell-like redirections (e.g. 2>&1) before checking
|
||||
const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, '').trim()
|
||||
const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim()
|
||||
return isAllowedSingleCommand(cmdWithoutRedirection, allowedCommands)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function getContextMenuOptions(
|
||||
value: "git-changes",
|
||||
label: "Working changes",
|
||||
description: "Current uncommitted changes",
|
||||
icon: "$(git-commit)"
|
||||
icon: "$(git-commit)",
|
||||
}
|
||||
|
||||
if (query === "") {
|
||||
@@ -93,8 +93,7 @@ export function getContextMenuOptions(
|
||||
}
|
||||
|
||||
if (selectedType === ContextMenuOptionType.Git) {
|
||||
const commits = queryItems
|
||||
.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
|
||||
}
|
||||
|
||||
@@ -112,11 +111,11 @@ export function getContextMenuOptions(
|
||||
|
||||
// Check for top-level option matches
|
||||
if ("git".startsWith(lowerQuery)) {
|
||||
suggestions.push({
|
||||
suggestions.push({
|
||||
type: ContextMenuOptionType.Git,
|
||||
label: "Git Commits",
|
||||
description: "Search repository history",
|
||||
icon: "$(git-commit)"
|
||||
icon: "$(git-commit)",
|
||||
})
|
||||
} else if ("git-changes".startsWith(lowerQuery)) {
|
||||
suggestions.push(workingChanges)
|
||||
@@ -130,9 +129,8 @@ export function getContextMenuOptions(
|
||||
|
||||
// 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
|
||||
const exactMatches = queryItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery,
|
||||
)
|
||||
if (exactMatches.length > 0) {
|
||||
suggestions.push(...exactMatches)
|
||||
@@ -143,52 +141,50 @@ export function getContextMenuOptions(
|
||||
value: lowerQuery,
|
||||
label: `Commit ${lowerQuery}`,
|
||||
description: "Git commit hash",
|
||||
icon: "$(git-commit)"
|
||||
icon: "$(git-commit)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create searchable strings array for fzf
|
||||
const searchableItems = queryItems.map(item => ({
|
||||
const searchableItems = queryItems.map((item) => ({
|
||||
original: item,
|
||||
searchStr: [item.value, item.label, item.description].filter(Boolean).join(' ')
|
||||
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
|
||||
selector: (item) => item.searchStr,
|
||||
})
|
||||
|
||||
// Get fuzzy matching items
|
||||
const matchingItems = query ? fzf.find(query).map(result => result.item.original) : []
|
||||
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.Folder
|
||||
const fileMatches = matchingItems.filter(
|
||||
(item) => item.type === ContextMenuOptionType.File || 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.Folder &&
|
||||
item.type !== ContextMenuOptionType.Git
|
||||
const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
|
||||
const otherMatches = matchingItems.filter(
|
||||
(item) =>
|
||||
item.type !== ContextMenuOptionType.File &&
|
||||
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 deduped = allItems.filter((item) => {
|
||||
const key = `${item.type}-${item.value}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export function highlightFzfMatch(text: string, positions: number[], highlightClassName: string = "history-item-highlight") {
|
||||
export function highlightFzfMatch(
|
||||
text: string,
|
||||
positions: number[],
|
||||
highlightClassName: string = "history-item-highlight",
|
||||
) {
|
||||
if (!positions.length) return text
|
||||
|
||||
const parts: { text: string; highlight: boolean }[] = []
|
||||
@@ -12,14 +16,14 @@ export function highlightFzfMatch(text: string, positions: number[], highlightCl
|
||||
if (pos > lastIndex) {
|
||||
parts.push({
|
||||
text: text.substring(lastIndex, pos),
|
||||
highlight: false
|
||||
highlight: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Add highlighted character
|
||||
parts.push({
|
||||
text: text[pos],
|
||||
highlight: true
|
||||
highlight: true,
|
||||
})
|
||||
|
||||
lastIndex = pos + 1
|
||||
@@ -29,16 +33,12 @@ export function highlightFzfMatch(text: string, positions: number[], highlightCl
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
text: text.substring(lastIndex),
|
||||
highlight: false
|
||||
highlight: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Build final string
|
||||
return parts
|
||||
.map(part =>
|
||||
part.highlight
|
||||
? `<span class="${highlightClassName}">${part.text}</span>`
|
||||
: part.text
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
.map((part) => (part.highlight ? `<span class="${highlightClassName}">${part.text}</span>` : part.text))
|
||||
.join("")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user