Selaa lähdekoodia

Revert "feat: syntax highlighting terminal output with Shiki" (#3023)

Revert "feat: syntax highlighting terminal output with Shiki (#3021)"

This reverts commit 8b072ad126272e1443f512c99e1c6e790fee5dfc.
Chris Estreich 8 kuukautta sitten
vanhempi
sitoutus
60270d1064

+ 36 - 39
webview-ui/src/components/chat/CommandExecution.tsx

@@ -1,9 +1,9 @@
-import { forwardRef, useState } from "react"
-import { useTranslation } from "react-i18next"
+import { HTMLAttributes, forwardRef, useMemo, useState } from "react"
+import { Virtuoso } from "react-virtuoso"
+import { ChevronDown } from "lucide-react"
 
 import { useExtensionState } from "@src/context/ExtensionStateContext"
-import { BaseTerminal } from "../../../../src/integrations/terminal/BaseTerminal"
-import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
+import { cn } from "@src/lib/utils"
 
 interface CommandExecutionProps {
 	command: string
@@ -11,48 +11,45 @@ interface CommandExecutionProps {
 }
 
 export const CommandExecution = forwardRef<HTMLDivElement, CommandExecutionProps>(({ command, output }, ref) => {
-	const { t } = useTranslation()
-	const { terminalShellIntegrationDisabled = false, terminalOutputLineLimit = 500 } = useExtensionState()
+	const { terminalShellIntegrationDisabled = false } = useExtensionState()
+
+	// If we aren't opening the VSCode terminal for this command then we default
+	// to expanding the command execution output.
 	const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
-	const compressedOutput = BaseTerminal.compressTerminalOutput(output, terminalOutputLineLimit)
 
-	const onToggleExpand = () => {
-		setIsExpanded(!isExpanded)
-	}
+	const lines = useMemo(() => output.split("\n"), [output])
 
 	return (
-		<>
+		<div ref={ref} className="w-full p-2 rounded-xs bg-vscode-editor-background">
 			<div
-				ref={ref}
-				style={{
-					borderRadius: 3,
-					border: "1px solid var(--vscode-editorGroup-border)",
-					overflow: "hidden",
-					backgroundColor: CODE_BLOCK_BG_COLOR,
-				}}>
-				<CodeBlock source={command} language="shell" />
-				{output.length > 0 && (
-					<div style={{ width: "100%" }}>
-						<div
-							onClick={onToggleExpand}
-							style={{
-								display: "flex",
-								alignItems: "center",
-								gap: "4px",
-								width: "100%",
-								justifyContent: "flex-start",
-								cursor: "pointer",
-								padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
-							}}>
-							<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
-							<span style={{ fontSize: "0.8em" }}>{t("chat:commandOutput")}</span>
-						</div>
-						{isExpanded && <CodeBlock source={compressedOutput} language="log" />}
-					</div>
-				)}
+				className={cn("flex flex-row justify-between cursor-pointer active:opacity-75", {
+					"opacity-50": isExpanded,
+				})}
+				onClick={() => setIsExpanded(!isExpanded)}>
+				<Line>{command}</Line>
+				<ChevronDown className={cn("size-4 transition-transform duration-300", { "rotate-180": isExpanded })} />
+			</div>
+			<div className={cn("h-[200px]", { hidden: !isExpanded })}>
+				<Virtuoso
+					className="h-full mt-2"
+					totalCount={lines.length}
+					itemContent={(i) => <Line>{lines[i]}</Line>}
+					followOutput="auto"
+				/>
 			</div>
-		</>
+		</div>
 	)
 })
 
+type LineProps = HTMLAttributes<HTMLDivElement>
+
+const Line = ({ className, ...props }: LineProps) => {
+	return (
+		<div
+			className={cn("font-mono text-vscode-editor-foreground whitespace-pre-wrap break-words", className)}
+			{...props}
+		/>
+	)
+}
+
 CommandExecution.displayName = "CommandExecution"

+ 28 - 23
webview-ui/src/components/chat/__tests__/CommandExecution.test.tsx

@@ -1,18 +1,33 @@
 // npx jest src/components/chat/__tests__/CommandExecution.test.tsx
 
 import React from "react"
-import { render, screen, fireEvent } from "@testing-library/react"
+import { render, screen } from "@testing-library/react"
 
 import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"
 
 import { CommandExecution } from "../CommandExecution"
 
-jest.mock("../../../components/common/CodeBlock")
-
 jest.mock("@src/lib/utils", () => ({
 	cn: (...inputs: any[]) => inputs.filter(Boolean).join(" "),
 }))
 
+jest.mock("lucide-react", () => ({
+	ChevronDown: () => <div data-testid="chevron-down">ChevronDown</div>,
+}))
+
+jest.mock("react-virtuoso", () => ({
+	Virtuoso: React.forwardRef(({ totalCount, itemContent }: any, ref: any) => (
+		<div ref={ref} data-testid="virtuoso-container">
+			{Array.from({ length: totalCount }).map((_, index) => (
+				<div key={index} data-testid={`virtuoso-item-${index}`}>
+					{itemContent(index)}
+				</div>
+			))}
+		</div>
+	)),
+	VirtuosoHandle: jest.fn(),
+}))
+
 describe("CommandExecution", () => {
 	const renderComponent = (command: string, output: string) => {
 		return render(
@@ -25,34 +40,24 @@ describe("CommandExecution", () => {
 	it("renders command output with virtualized list", () => {
 		const testOutput = "Line 1\nLine 2\nLine 3"
 		renderComponent("ls", testOutput)
-		const codeBlock = screen.getByTestId("mock-code-block")
-		expect(codeBlock).toHaveTextContent("ls")
-
-		fireEvent.click(screen.getByText("commandOutput"))
-		const outputBlock = screen.getAllByTestId("mock-code-block")[1]
-
-		expect(outputBlock).toHaveTextContent("Line 1")
-		expect(outputBlock).toHaveTextContent("Line 2")
-		expect(outputBlock).toHaveTextContent("Line 3")
+		expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+		expect(screen.getByText("Line 1")).toBeInTheDocument()
+		expect(screen.getByText("Line 2")).toBeInTheDocument()
+		expect(screen.getByText("Line 3")).toBeInTheDocument()
 	})
 
 	it("handles empty output", () => {
 		renderComponent("ls", "")
-		const codeBlock = screen.getByTestId("mock-code-block")
-		expect(codeBlock).toHaveTextContent("ls")
-		expect(screen.queryByText("commandOutput")).not.toBeInTheDocument()
-		expect(screen.queryAllByTestId("mock-code-block")).toHaveLength(1)
+		expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+		expect(screen.getByTestId("virtuoso-item-0")).toBeInTheDocument()
+		expect(screen.queryByTestId("virtuoso-item-1")).not.toBeInTheDocument()
 	})
 
 	it("handles large output", () => {
 		const largeOutput = Array.from({ length: 1000 }, (_, i) => `Line ${i + 1}`).join("\n")
 		renderComponent("ls", largeOutput)
-		const codeBlock = screen.getByTestId("mock-code-block")
-		expect(codeBlock).toHaveTextContent("ls")
-
-		fireEvent.click(screen.getByText("commandOutput"))
-		const outputBlock = screen.getAllByTestId("mock-code-block")[1]
-		expect(outputBlock).toHaveTextContent("Line 1")
-		expect(outputBlock).toHaveTextContent("Line 1000")
+		expect(screen.getByTestId("virtuoso-container")).toBeInTheDocument()
+		expect(screen.getByText("Line 1")).toBeInTheDocument()
+		expect(screen.getByText("Line 1000")).toBeInTheDocument()
 	})
 })

+ 2 - 2
webview-ui/src/components/common/__mocks__/CodeBlock.tsx

@@ -1,10 +1,10 @@
 import * as React from "react"
 
 interface CodeBlockProps {
-	source?: string
+	children?: React.ReactNode
 	language?: string
 }
 
-const CodeBlock: React.FC<CodeBlockProps> = ({ source = "" }) => <div data-testid="mock-code-block">{source}</div>
+const CodeBlock: React.FC<CodeBlockProps> = () => <div data-testid="mock-code-block">Mocked Code Block</div>
 
 export default CodeBlock