mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
test(vscode): extract fileName, dirName, buildHighlightSegments from PromptInput and add tests
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { fileName, dirName, buildHighlightSegments } from "../../webview-ui/src/components/chat/prompt-input-utils"
|
||||
|
||||
describe("fileName", () => {
|
||||
it("extracts the last segment of a unix path", () => {
|
||||
expect(fileName("src/components/chat/PromptInput.tsx")).toBe("PromptInput.tsx")
|
||||
})
|
||||
|
||||
it("extracts the last segment of a Windows path", () => {
|
||||
expect(fileName("src\\components\\chat\\PromptInput.tsx")).toBe("PromptInput.tsx")
|
||||
})
|
||||
|
||||
it("returns the path itself when no separator present", () => {
|
||||
expect(fileName("README.md")).toBe("README.md")
|
||||
})
|
||||
|
||||
it("returns the filename for a single directory segment", () => {
|
||||
expect(fileName("src/foo.ts")).toBe("foo.ts")
|
||||
})
|
||||
|
||||
it("handles mixed separators", () => {
|
||||
expect(fileName("src\\components/chat/File.tsx")).toBe("File.tsx")
|
||||
})
|
||||
})
|
||||
|
||||
describe("dirName", () => {
|
||||
it("returns empty string for a file with no directory", () => {
|
||||
expect(dirName("README.md")).toBe("")
|
||||
})
|
||||
|
||||
it("returns the directory for a simple path", () => {
|
||||
expect(dirName("src/foo.ts")).toBe("src")
|
||||
})
|
||||
|
||||
it("returns full directory for a short path", () => {
|
||||
expect(dirName("src/components/foo.ts")).toBe("src/components")
|
||||
})
|
||||
|
||||
it("truncates long directories to last two segments", () => {
|
||||
const path = "packages/kilo-vscode/webview-ui/src/components/chat/foo.ts"
|
||||
const result = dirName(path)
|
||||
expect(result).toMatch(/^…\//)
|
||||
expect(result).toContain("components/chat")
|
||||
})
|
||||
|
||||
it("does not truncate directories at exactly 30 chars", () => {
|
||||
const dir = "a".repeat(15) + "/" + "b".repeat(14)
|
||||
const result = dirName(`${dir}/file.ts`)
|
||||
expect(result).toBe(dir)
|
||||
})
|
||||
|
||||
it("truncates directories longer than 30 chars", () => {
|
||||
const dir = "a".repeat(16) + "/" + "b".repeat(15)
|
||||
const result = dirName(`${dir}/file.ts`)
|
||||
expect(result.startsWith("…/")).toBe(true)
|
||||
})
|
||||
|
||||
it("normalizes Windows backslashes before measuring length", () => {
|
||||
const result = dirName("src\\foo.ts")
|
||||
expect(result).toBe("src")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildHighlightSegments", () => {
|
||||
it("returns single non-highlighted segment when paths set is empty", () => {
|
||||
const result = buildHighlightSegments("hello world", new Set())
|
||||
expect(result).toEqual([{ text: "hello world", highlight: false }])
|
||||
})
|
||||
|
||||
it("returns single non-highlighted segment when no mention present", () => {
|
||||
const result = buildHighlightSegments("hello world", new Set(["foo.ts"]))
|
||||
expect(result).toEqual([{ text: "hello world", highlight: false }])
|
||||
})
|
||||
|
||||
it("highlights a single mention token", () => {
|
||||
const result = buildHighlightSegments("@foo.ts", new Set(["foo.ts"]))
|
||||
expect(result).toEqual([{ text: "@foo.ts", highlight: true }])
|
||||
})
|
||||
|
||||
it("splits text before and highlight token", () => {
|
||||
const result = buildHighlightSegments("see @foo.ts here", new Set(["foo.ts"]))
|
||||
expect(result).toEqual([
|
||||
{ text: "see ", highlight: false },
|
||||
{ text: "@foo.ts", highlight: true },
|
||||
{ text: " here", highlight: false },
|
||||
])
|
||||
})
|
||||
|
||||
it("highlights multiple mentions in order", () => {
|
||||
const result = buildHighlightSegments("@a.ts and @b.ts done", new Set(["a.ts", "b.ts"]))
|
||||
expect(result).toEqual([
|
||||
{ text: "@a.ts", highlight: true },
|
||||
{ text: " and ", highlight: false },
|
||||
{ text: "@b.ts", highlight: true },
|
||||
{ text: " done", highlight: false },
|
||||
])
|
||||
})
|
||||
|
||||
it("picks the earliest mention when multiple paths could match", () => {
|
||||
const result = buildHighlightSegments("@b.ts then @a.ts", new Set(["a.ts", "b.ts"]))
|
||||
expect(result[0]).toEqual({ text: "@b.ts", highlight: true })
|
||||
expect(result[2]).toEqual({ text: "@a.ts", highlight: true })
|
||||
})
|
||||
|
||||
it("handles back-to-back mentions with no separator", () => {
|
||||
const result = buildHighlightSegments("@a.ts@b.ts", new Set(["a.ts", "b.ts"]))
|
||||
const highlighted = result.filter((s) => s.highlight)
|
||||
expect(highlighted).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("returns empty array for empty string", () => {
|
||||
const result = buildHighlightSegments("", new Set(["foo.ts"]))
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("does not partially match longer paths", () => {
|
||||
const result = buildHighlightSegments("@foo.ts", new Set(["foo.tsx"]))
|
||||
expect(result).toEqual([{ text: "@foo.ts", highlight: false }])
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,7 @@ import { ModelSelector } from "./ModelSelector"
|
||||
import { ModeSwitcher } from "./ModeSwitcher"
|
||||
import { useFileMention } from "../../hooks/useFileMention"
|
||||
import { useImageAttachments } from "../../hooks/useImageAttachments"
|
||||
import { fileName, dirName, buildHighlightSegments } from "./prompt-input-utils"
|
||||
|
||||
const AUTOCOMPLETE_DEBOUNCE_MS = 500
|
||||
const MIN_TEXT_LENGTH = 3
|
||||
@@ -150,43 +151,6 @@ export const PromptInput: Component = () => {
|
||||
textareaRef.style.height = `${Math.min(textareaRef.scrollHeight, 200)}px`
|
||||
}
|
||||
|
||||
const buildHighlightSegments = (val: string) => {
|
||||
const paths = mention.mentionedPaths()
|
||||
if (paths.size === 0) return [{ text: val, highlight: false }]
|
||||
|
||||
const segments: { text: string; highlight: boolean }[] = []
|
||||
let remaining = val
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let earliest = -1
|
||||
let earliestPath = ""
|
||||
|
||||
for (const path of paths) {
|
||||
const token = `@${path}`
|
||||
const idx = remaining.indexOf(token)
|
||||
if (idx !== -1 && (earliest === -1 || idx < earliest)) {
|
||||
earliest = idx
|
||||
earliestPath = path
|
||||
}
|
||||
}
|
||||
|
||||
if (earliest === -1) {
|
||||
segments.push({ text: remaining, highlight: false })
|
||||
break
|
||||
}
|
||||
|
||||
if (earliest > 0) {
|
||||
segments.push({ text: remaining.substring(0, earliest), highlight: false })
|
||||
}
|
||||
|
||||
const token = `@${earliestPath}`
|
||||
segments.push({ text: token, highlight: true })
|
||||
remaining = remaining.substring(earliest + token.length)
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
const handleInput = (e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
const val = target.value
|
||||
@@ -263,14 +227,6 @@ export const PromptInput: Component = () => {
|
||||
if (textareaRef) textareaRef.style.height = "auto"
|
||||
}
|
||||
|
||||
const fileName = (path: string) => path.replaceAll("\\", "/").split("/").pop() ?? path
|
||||
const dirName = (path: string) => {
|
||||
const parts = path.replaceAll("\\", "/").split("/")
|
||||
if (parts.length <= 1) return ""
|
||||
const dir = parts.slice(0, -1).join("/")
|
||||
return dir.length > 30 ? `…/${parts.slice(-3, -1).join("/")}` : dir
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class="prompt-input-container"
|
||||
@@ -327,7 +283,7 @@ export const PromptInput: Component = () => {
|
||||
<div class="prompt-input-wrapper">
|
||||
<div class="prompt-input-ghost-wrapper">
|
||||
<div class="prompt-input-highlight-overlay" ref={highlightRef} aria-hidden="true">
|
||||
<Index each={buildHighlightSegments(text())}>
|
||||
<Index each={buildHighlightSegments(text(), mention.mentionedPaths())}>
|
||||
{(seg) => (
|
||||
<Show when={seg().highlight} fallback={<span>{seg().text}</span>}>
|
||||
<span class="prompt-input-file-mention">{seg().text}</span>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
export function fileName(path: string): string {
|
||||
return path.replaceAll("\\", "/").split("/").pop() ?? path
|
||||
}
|
||||
|
||||
export function dirName(path: string): string {
|
||||
const parts = path.replaceAll("\\", "/").split("/")
|
||||
if (parts.length <= 1) return ""
|
||||
const dir = parts.slice(0, -1).join("/")
|
||||
return dir.length > 30 ? `…/${parts.slice(-3, -1).join("/")}` : dir
|
||||
}
|
||||
|
||||
export function buildHighlightSegments(val: string, paths: Set<string>): { text: string; highlight: boolean }[] {
|
||||
if (paths.size === 0) return [{ text: val, highlight: false }]
|
||||
|
||||
const segments: { text: string; highlight: boolean }[] = []
|
||||
let remaining = val
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let earliest = -1
|
||||
let earliestPath = ""
|
||||
|
||||
for (const path of paths) {
|
||||
const token = `@${path}`
|
||||
const idx = remaining.indexOf(token)
|
||||
if (idx !== -1 && (earliest === -1 || idx < earliest)) {
|
||||
earliest = idx
|
||||
earliestPath = path
|
||||
}
|
||||
}
|
||||
|
||||
if (earliest === -1) {
|
||||
segments.push({ text: remaining, highlight: false })
|
||||
break
|
||||
}
|
||||
|
||||
if (earliest > 0) {
|
||||
segments.push({ text: remaining.substring(0, earliest), highlight: false })
|
||||
}
|
||||
|
||||
const token = `@${earliestPath}`
|
||||
segments.push({ text: token, highlight: true })
|
||||
remaining = remaining.substring(earliest + token.length)
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
Reference in New Issue
Block a user