feat: add tooltip, button components and editor utility functions for enhanced code editing experience

This commit is contained in:
ngc2207 2025-01-10 22:47:29 +08:00
parent 1aeb472495
commit 3587cfe009
6 changed files with 282 additions and 48 deletions

Binary file not shown.

View File

@ -13,7 +13,9 @@
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"devicons-react": "^1.4.0", "devicons-react": "^1.4.0",

View File

@ -10,7 +10,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Palette } from "lucide-react"; import {
Copy,
FileDown,
Paintbrush,
Palette,
Redo2,
Undo2,
} from "lucide-react";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import { highlighter } from "@/lib/shiki"; import { highlighter } from "@/lib/shiki";
import { Bot, CodeXml } from "lucide-react"; import { Bot, CodeXml } from "lucide-react";
@ -23,6 +30,21 @@ import { SUPPORTED_EDITOR_LANGUAGES_CONFIG } from "@/constants/languages";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp"; import { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
redoChange,
undoChange,
formatCode,
downloadCode,
copyCode,
moveCursorToEnd,
} from "@/lib/editor";
self.MonacoEnvironment = { self.MonacoEnvironment = {
getWorker(_, _label) { getWorker(_, _label) {
@ -37,23 +59,42 @@ function App() {
const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME); const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME);
const file = DEFAULT_FILES[language]; const file = DEFAULT_FILES[language];
const webSocketRef = useRef<WebSocket | null>(null); const webSocketRef = useRef<WebSocket | null>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
useEffect(() => { useEffect(() => {
if (webSocketRef.current) { const closeWebSocket = () => {
webSocketRef.current.close();
}
if (language in SUPPORTED_LSP_LANGUAGES) {
connectToLanguageServer(language).then((webSocket) => {
webSocketRef.current = webSocket;
});
}
return () => {
if (webSocketRef.current) { if (webSocketRef.current) {
webSocketRef.current.close(); if (
webSocketRef.current.readyState === WebSocket.OPEN ||
webSocketRef.current.readyState === WebSocket.CONNECTING
) {
webSocketRef.current.onclose = () => {
webSocketRef.current = null;
};
webSocketRef.current.close();
} else {
webSocketRef.current = null;
}
} }
}; };
const connectWebSocket = async () => {
closeWebSocket();
if (editorRef.current && language in SUPPORTED_LSP_LANGUAGES) {
try {
const webSocket = await connectToLanguageServer(language);
webSocket.onopen = () => {
webSocketRef.current = webSocket;
};
} catch (error) {
console.error("Failed to connect to language server", error);
}
}
};
connectWebSocket();
return closeWebSocket;
}, [language]); }, [language]);
return ( return (
@ -74,18 +115,6 @@ function App() {
/> />
</TabsTrigger> </TabsTrigger>
<TabsTrigger
value="diff-editor"
className="500 rounded-full data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-none"
>
<Bot
className="-ms-0.5 me-1.5 opacity-60"
size={16}
strokeWidth={2}
aria-hidden="true"
/>
AI
</TabsTrigger>
</TabsList> </TabsList>
<div className="flex items-center gap-x-2 pr-4 mb-2"> <div className="flex items-center gap-x-2 pr-4 mb-2">
<Select value={theme} onValueChange={setTheme}> <Select value={theme} onValueChange={setTheme}>
@ -123,6 +152,98 @@ function App() {
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
</ScrollArea> </ScrollArea>
<TabsContent value="code-editor" className="h-full mt-0"> <TabsContent value="code-editor" className="h-full mt-0">
<div className="flex items-center justify-end gap-x-2 pr-4">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Redo"
className="bg-white"
onClick={() => redoChange(editorRef.current!)}
>
<Redo2 size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Undo"
className="bg-white"
onClick={() => undoChange(editorRef.current!)}
>
<Undo2 size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Format"
className="bg-white"
onClick={() => formatCode(editorRef.current!)}
>
<Paintbrush size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Copy"
className="bg-white"
onClick={() => copyCode(editorRef.current!)}
>
<Copy size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label="Download"
className="bg-white"
onClick={() => downloadCode(editorRef.current!)}
>
<FileDown size={16} strokeWidth={2} aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent className="px-2 py-1 text-xs">
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Editor <Editor
theme={theme} theme={theme}
path={file.path} path={file.path}
@ -145,6 +266,7 @@ function App() {
shikiToMonaco(await highlighter, monaco); shikiToMonaco(await highlighter, monaco);
}} }}
onMount={(editor: monaco.editor.IStandaloneCodeEditor) => { onMount={(editor: monaco.editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
const value = editor.getModel()?.getValue(); const value = editor.getModel()?.getValue();
if (value !== undefined) { if (value !== undefined) {
window.parent.postMessage( window.parent.postMessage(
@ -168,29 +290,6 @@ function App() {
}} }}
/> />
</TabsContent> </TabsContent>
<TabsContent value="diff-editor" className="h-full mt-0">
<DiffEditor
theme={theme}
language={file.language}
original={file.value}
modified={file.value}
options={{
minimap: { enabled: false },
fontSize: 16,
showFoldingControls: "always",
automaticLayout: true,
autoIndent: "full",
guides: {
bracketPairs: true,
indentation: true,
},
padding: { top: 8 },
}}
beforeMount={async (monaco: Monaco) => {
shikiToMonaco(await highlighter, monaco);
}}
/>
</TabsContent>
</Tabs> </Tabs>
</div> </div>
); );

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,30 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,46 @@
import * as monaco from "monaco-editor";
export function redoChange(editor: monaco.editor.IStandaloneCodeEditor) {
editor.trigger("redo", "redo", null);
}
export function undoChange(editor: monaco.editor.IStandaloneCodeEditor) {
editor.trigger("undo", "undo", null);
}
export function formatCode(editor: monaco.editor.IStandaloneCodeEditor) {
editor.trigger("format", "editor.action.formatDocument", null);
}
export function downloadCode(editor: monaco.editor.IStandaloneCodeEditor) {
const model = editor.getModel();
if (model) {
const code = model.getValue();
const blob = new Blob([code], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const fileName = model.uri.path.split("/").pop()?.replace(/_/g, "");
a.download = fileName || "code.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
export function copyCode(editor: monaco.editor.IStandaloneCodeEditor) {
const model = editor.getModel();
if (model) {
navigator.clipboard.writeText(model.getValue());
}
}
export function moveCursorToEnd(editor: monaco.editor.IStandaloneCodeEditor) {
const model = editor.getModel();
if (model) {
const position = model.getPositionAt(model.getValueLength());
editor.setPosition(position);
editor.focus();
}
}