mirror of
https://litchi.icu/ngc2207/judge.git
synced 2025-05-18 22:27:44 +00:00
feat: add tooltip, button components and editor utility functions for enhanced code editing experience
This commit is contained in:
parent
1aeb472495
commit
3587cfe009
Binary file not shown.
@ -13,7 +13,9 @@
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"devicons-react": "^1.4.0",
|
||||
|
@ -10,7 +10,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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 { highlighter } from "@/lib/shiki";
|
||||
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 { connectToLanguageServer, SUPPORTED_LSP_LANGUAGES } from "@/lib/lsp";
|
||||
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 = {
|
||||
getWorker(_, _label) {
|
||||
@ -37,23 +59,42 @@ function App() {
|
||||
const [theme, setTheme] = useState(DEFAULT_EDITOR_THEME);
|
||||
const file = DEFAULT_FILES[language];
|
||||
const webSocketRef = useRef<WebSocket | null>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const closeWebSocket = () => {
|
||||
if (webSocketRef.current) {
|
||||
if (
|
||||
webSocketRef.current.readyState === WebSocket.OPEN ||
|
||||
webSocketRef.current.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
webSocketRef.current.onclose = () => {
|
||||
webSocketRef.current = null;
|
||||
};
|
||||
webSocketRef.current.close();
|
||||
} else {
|
||||
webSocketRef.current = null;
|
||||
}
|
||||
|
||||
if (language in SUPPORTED_LSP_LANGUAGES) {
|
||||
connectToLanguageServer(language).then((webSocket) => {
|
||||
webSocketRef.current = webSocket;
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
@ -74,18 +115,6 @@ function App() {
|
||||
/>
|
||||
代码
|
||||
</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>
|
||||
<div className="flex items-center gap-x-2 pr-4 mb-2">
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
@ -123,6 +152,98 @@ function App() {
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
<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
|
||||
theme={theme}
|
||||
path={file.path}
|
||||
@ -145,6 +266,7 @@ function App() {
|
||||
shikiToMonaco(await highlighter, monaco);
|
||||
}}
|
||||
onMount={(editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
const value = editor.getModel()?.getValue();
|
||||
if (value !== undefined) {
|
||||
window.parent.postMessage(
|
||||
@ -168,29 +290,6 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
57
code-editor/src/components/ui/button.tsx
Normal file
57
code-editor/src/components/ui/button.tsx
Normal 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 }
|
30
code-editor/src/components/ui/tooltip.tsx
Normal file
30
code-editor/src/components/ui/tooltip.tsx
Normal 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 }
|
46
code-editor/src/lib/editor.ts
Normal file
46
code-editor/src/lib/editor.ts
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user