mirror of
https://litchi.icu/ngc2207/judge.git
synced 2025-05-18 22:37:11 +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",
|
"@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",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
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