mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(ipuaro): add multiline input and syntax highlighting
- Multiline input support with Shift+Enter for new lines - Auto-height adjustment and line navigation - Syntax highlighting in DiffView for added lines - Language detection from file extensions - Config options for multiline and syntaxHighlight
This commit is contained in:
@@ -181,4 +181,170 @@ describe("Input", () => {
|
||||
expect(savedInput).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiline support", () => {
|
||||
describe("InputProps with multiline", () => {
|
||||
it("should accept multiline as boolean", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: false,
|
||||
multiline: true,
|
||||
}
|
||||
expect(props.multiline).toBe(true)
|
||||
})
|
||||
|
||||
it("should accept multiline as 'auto'", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: false,
|
||||
multiline: "auto",
|
||||
}
|
||||
expect(props.multiline).toBe("auto")
|
||||
})
|
||||
|
||||
it("should have multiline false by default", () => {
|
||||
const props: InputProps = {
|
||||
onSubmit: vi.fn(),
|
||||
history: [],
|
||||
disabled: false,
|
||||
}
|
||||
expect(props.multiline).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiline activation logic", () => {
|
||||
it("should be active when multiline is true", () => {
|
||||
const multiline = true
|
||||
const lines = ["single line"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be active when multiline is false", () => {
|
||||
const multiline = false
|
||||
const lines = ["line1", "line2"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(false)
|
||||
})
|
||||
|
||||
it("should be active in auto mode with multiple lines", () => {
|
||||
const multiline = "auto"
|
||||
const lines = ["line1", "line2"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be active in auto mode with single line", () => {
|
||||
const multiline = "auto"
|
||||
const lines = ["single line"]
|
||||
const isMultilineActive = multiline === true || (multiline === "auto" && lines.length > 1)
|
||||
expect(isMultilineActive).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("line management", () => {
|
||||
it("should update current line on change", () => {
|
||||
const lines = ["first", "second", "third"]
|
||||
const currentLineIndex = 1
|
||||
const newValue = "updated second"
|
||||
|
||||
const newLines = [...lines]
|
||||
newLines[currentLineIndex] = newValue
|
||||
|
||||
expect(newLines).toEqual(["first", "updated second", "third"])
|
||||
expect(newLines.join("\n")).toBe("first\nupdated second\nthird")
|
||||
})
|
||||
|
||||
it("should add new line at current position", () => {
|
||||
const lines = ["first", "second"]
|
||||
const currentLineIndex = 0
|
||||
|
||||
const newLines = [...lines]
|
||||
newLines.splice(currentLineIndex + 1, 0, "")
|
||||
|
||||
expect(newLines).toEqual(["first", "", "second"])
|
||||
})
|
||||
|
||||
it("should join lines with newline for submit", () => {
|
||||
const lines = ["line 1", "line 2", "line 3"]
|
||||
const fullText = lines.join("\n")
|
||||
expect(fullText).toBe("line 1\nline 2\nline 3")
|
||||
})
|
||||
})
|
||||
|
||||
describe("line navigation", () => {
|
||||
it("should navigate up in multiline mode", () => {
|
||||
const lines = ["line1", "line2", "line3"]
|
||||
let currentLineIndex = 2
|
||||
|
||||
currentLineIndex = currentLineIndex - 1
|
||||
expect(currentLineIndex).toBe(1)
|
||||
|
||||
currentLineIndex = currentLineIndex - 1
|
||||
expect(currentLineIndex).toBe(0)
|
||||
})
|
||||
|
||||
it("should not navigate up past first line", () => {
|
||||
const lines = ["line1", "line2"]
|
||||
const currentLineIndex = 0
|
||||
const isMultilineActive = true
|
||||
|
||||
const canNavigateUp = isMultilineActive && currentLineIndex > 0
|
||||
expect(canNavigateUp).toBe(false)
|
||||
})
|
||||
|
||||
it("should navigate down in multiline mode", () => {
|
||||
const lines = ["line1", "line2", "line3"]
|
||||
let currentLineIndex = 0
|
||||
|
||||
currentLineIndex = currentLineIndex + 1
|
||||
expect(currentLineIndex).toBe(1)
|
||||
|
||||
currentLineIndex = currentLineIndex + 1
|
||||
expect(currentLineIndex).toBe(2)
|
||||
})
|
||||
|
||||
it("should not navigate down past last line", () => {
|
||||
const lines = ["line1", "line2"]
|
||||
const currentLineIndex = 1
|
||||
const isMultilineActive = true
|
||||
|
||||
const canNavigateDown = isMultilineActive && currentLineIndex < lines.length - 1
|
||||
expect(canNavigateDown).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("multiline submit", () => {
|
||||
it("should submit trimmed multiline text", () => {
|
||||
const lines = ["line 1", "line 2", "line 3"]
|
||||
const fullText = lines.join("\n").trim()
|
||||
expect(fullText).toBe("line 1\nline 2\nline 3")
|
||||
})
|
||||
|
||||
it("should not submit empty multiline text", () => {
|
||||
const onSubmit = vi.fn()
|
||||
const lines = ["", "", ""]
|
||||
const fullText = lines.join("\n").trim()
|
||||
|
||||
if (fullText) {
|
||||
onSubmit(fullText)
|
||||
}
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should reset lines after submit", () => {
|
||||
let lines = ["line1", "line2"]
|
||||
let currentLineIndex = 1
|
||||
|
||||
lines = [""]
|
||||
currentLineIndex = 0
|
||||
|
||||
expect(lines).toEqual([""])
|
||||
expect(currentLineIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
155
packages/ipuaro/tests/unit/tui/utils/syntax-highlighter.test.ts
Normal file
155
packages/ipuaro/tests/unit/tui/utils/syntax-highlighter.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Tests for syntax-highlighter utility.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { detectLanguage, highlightLine } from "../../../../src/tui/utils/syntax-highlighter.js"
|
||||
|
||||
describe("syntax-highlighter", () => {
|
||||
describe("detectLanguage", () => {
|
||||
it("should detect typescript from .ts extension", () => {
|
||||
expect(detectLanguage("src/index.ts")).toBe("typescript")
|
||||
})
|
||||
|
||||
it("should detect tsx from .tsx extension", () => {
|
||||
expect(detectLanguage("src/Component.tsx")).toBe("tsx")
|
||||
})
|
||||
|
||||
it("should detect javascript from .js extension", () => {
|
||||
expect(detectLanguage("dist/bundle.js")).toBe("javascript")
|
||||
})
|
||||
|
||||
it("should detect jsx from .jsx extension", () => {
|
||||
expect(detectLanguage("src/App.jsx")).toBe("jsx")
|
||||
})
|
||||
|
||||
it("should detect json from .json extension", () => {
|
||||
expect(detectLanguage("package.json")).toBe("json")
|
||||
})
|
||||
|
||||
it("should detect yaml from .yaml extension", () => {
|
||||
expect(detectLanguage("config.yaml")).toBe("yaml")
|
||||
})
|
||||
|
||||
it("should detect yaml from .yml extension", () => {
|
||||
expect(detectLanguage("config.yml")).toBe("yaml")
|
||||
})
|
||||
|
||||
it("should return unknown for unsupported extensions", () => {
|
||||
expect(detectLanguage("image.png")).toBe("unknown")
|
||||
expect(detectLanguage("file")).toBe("unknown")
|
||||
})
|
||||
|
||||
it("should handle case insensitive extensions", () => {
|
||||
expect(detectLanguage("FILE.TS")).toBe("typescript")
|
||||
expect(detectLanguage("FILE.JSX")).toBe("jsx")
|
||||
})
|
||||
})
|
||||
|
||||
describe("highlightLine", () => {
|
||||
describe("unknown language", () => {
|
||||
it("should return plain text for unknown language", () => {
|
||||
const tokens = highlightLine("hello world", "unknown")
|
||||
expect(tokens).toEqual([{ text: "hello world", color: "white" }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("json language", () => {
|
||||
it("should return plain text for json", () => {
|
||||
const tokens = highlightLine('{"key": "value"}', "json")
|
||||
expect(tokens).toEqual([{ text: '{"key": "value"}', color: "white" }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("yaml language", () => {
|
||||
it("should return plain text for yaml", () => {
|
||||
const tokens = highlightLine("key: value", "yaml")
|
||||
expect(tokens).toEqual([{ text: "key: value", color: "white" }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("typescript/javascript highlighting", () => {
|
||||
it("should highlight keywords", () => {
|
||||
const tokens = highlightLine("const x = 10", "typescript")
|
||||
expect(tokens[0]).toEqual({ text: "const", color: "magenta" })
|
||||
expect(tokens.find((t) => t.text === "x")).toEqual({ text: "x", color: "white" })
|
||||
})
|
||||
|
||||
it("should highlight strings with double quotes", () => {
|
||||
const tokens = highlightLine('const s = "hello"', "typescript")
|
||||
expect(tokens.find((t) => t.text === '"hello"')).toEqual({
|
||||
text: '"hello"',
|
||||
color: "green",
|
||||
})
|
||||
})
|
||||
|
||||
it("should highlight strings with single quotes", () => {
|
||||
const tokens = highlightLine("const s = 'hello'", "typescript")
|
||||
expect(tokens.find((t) => t.text === "'hello'")).toEqual({
|
||||
text: "'hello'",
|
||||
color: "green",
|
||||
})
|
||||
})
|
||||
|
||||
it("should highlight template literals", () => {
|
||||
const tokens = highlightLine("const s = `hello`", "typescript")
|
||||
expect(tokens.find((t) => t.text === "`hello`")).toEqual({
|
||||
text: "`hello`",
|
||||
color: "green",
|
||||
})
|
||||
})
|
||||
|
||||
it("should highlight numbers", () => {
|
||||
const tokens = highlightLine("const n = 42", "typescript")
|
||||
expect(tokens.find((t) => t.text === "42")).toEqual({ text: "42", color: "cyan" })
|
||||
})
|
||||
|
||||
it("should highlight single-line comments", () => {
|
||||
const tokens = highlightLine("// this is a comment", "typescript")
|
||||
expect(tokens[0]).toEqual({ text: "// this is a comment", color: "gray" })
|
||||
})
|
||||
|
||||
it("should highlight multi-line comments", () => {
|
||||
const tokens = highlightLine("/* comment */", "typescript")
|
||||
expect(tokens[0]).toEqual({ text: "/* comment */", color: "gray" })
|
||||
})
|
||||
|
||||
it("should highlight operators", () => {
|
||||
const tokens = highlightLine("x + y = z", "typescript")
|
||||
expect(tokens.find((t) => t.text === "+")).toEqual({ text: "+", color: "yellow" })
|
||||
expect(tokens.find((t) => t.text === "=")).toEqual({ text: "=", color: "yellow" })
|
||||
})
|
||||
|
||||
it("should highlight parentheses and brackets", () => {
|
||||
const tokens = highlightLine("foo(bar[0])", "typescript")
|
||||
expect(tokens.find((t) => t.text === "(")).toEqual({ text: "(", color: "yellow" })
|
||||
expect(tokens.find((t) => t.text === "[")).toEqual({ text: "[", color: "yellow" })
|
||||
expect(tokens.find((t) => t.text === "]")).toEqual({ text: "]", color: "yellow" })
|
||||
expect(tokens.find((t) => t.text === ")")).toEqual({ text: ")", color: "yellow" })
|
||||
})
|
||||
|
||||
it("should handle mixed content", () => {
|
||||
const tokens = highlightLine('const x = "test" + 42', "typescript")
|
||||
expect(tokens.find((t) => t.text === "const")).toEqual({
|
||||
text: "const",
|
||||
color: "magenta",
|
||||
})
|
||||
expect(tokens.find((t) => t.text === '"test"')).toEqual({
|
||||
text: '"test"',
|
||||
color: "green",
|
||||
})
|
||||
expect(tokens.find((t) => t.text === "42")).toEqual({ text: "42", color: "cyan" })
|
||||
})
|
||||
|
||||
it("should preserve whitespace", () => {
|
||||
const tokens = highlightLine(" const x = 10 ", "typescript")
|
||||
expect(tokens[0]).toEqual({ text: " ", color: "white" })
|
||||
})
|
||||
|
||||
it("should handle empty lines", () => {
|
||||
const tokens = highlightLine("", "typescript")
|
||||
expect(tokens).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user