feat: add AI edit code selection
This commit is contained in:
parent
208d17879f
commit
62311faf51
@ -1,10 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
|
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
|
||||||
import monaco from "monaco-editor"
|
import * as monaco from "monaco-editor"
|
||||||
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useClerk } from "@clerk/nextjs"
|
import { useClerk } from "@clerk/nextjs"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
|
||||||
import * as Y from "yjs"
|
import * as Y from "yjs"
|
||||||
import LiveblocksProvider from "@liveblocks/yjs"
|
import LiveblocksProvider from "@liveblocks/yjs"
|
||||||
@ -17,7 +18,7 @@ import {
|
|||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable"
|
} from "@/components/ui/resizable"
|
||||||
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
|
import { FileJson, Loader2, Sparkles, TerminalSquare } from "lucide-react"
|
||||||
import Tab from "../ui/tab"
|
import Tab from "../ui/tab"
|
||||||
import Sidebar from "./sidebar"
|
import Sidebar from "./sidebar"
|
||||||
import GenerateInput from "./generate"
|
import GenerateInput from "./generate"
|
||||||
@ -30,8 +31,10 @@ import Loading from "./loading"
|
|||||||
import PreviewWindow from "./preview"
|
import PreviewWindow from "./preview"
|
||||||
import Terminals from "./terminals"
|
import Terminals from "./terminals"
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels"
|
import { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
|
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
|
||||||
import { useSocket } from "@/context/SocketContext"
|
import { useSocket } from "@/context/SocketContext"
|
||||||
|
import { Button } from "../ui/button"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
@ -40,9 +43,8 @@ export default function CodeEditor({
|
|||||||
userData: User
|
userData: User
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
//SocketContext functions and effects
|
//SocketContext functions and effects
|
||||||
const { socket, setUserAndSandboxId } = useSocket();
|
const { socket, setUserAndSandboxId } = useSocket()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||||
@ -50,10 +52,10 @@ export default function CodeEditor({
|
|||||||
// Check if the socket is not initialized or not connected
|
// Check if the socket is not initialized or not connected
|
||||||
if (!socket || (socket && !socket.connected)) {
|
if (!socket || (socket && !socket.connected)) {
|
||||||
// Initialize socket connection
|
// Initialize socket connection
|
||||||
setUserAndSandboxId(userData.id, sandboxData.id);
|
setUserAndSandboxId(userData.id, sandboxData.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]);
|
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId])
|
||||||
|
|
||||||
//Preview Button state
|
//Preview Button state
|
||||||
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
|
||||||
@ -89,7 +91,8 @@ export default function CodeEditor({
|
|||||||
options: monaco.editor.IModelDeltaDecoration[]
|
options: monaco.editor.IModelDeltaDecoration[]
|
||||||
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
instance: monaco.editor.IEditorDecorationsCollection | undefined
|
||||||
}>({ options: [], instance: undefined })
|
}>({ options: [], instance: undefined })
|
||||||
|
const [isSelected, setIsSelected] = useState(false)
|
||||||
|
const [showSuggestion, setShowSuggestion] = useState(false)
|
||||||
// Terminal state
|
// Terminal state
|
||||||
const [terminals, setTerminals] = useState<
|
const [terminals, setTerminals] = useState<
|
||||||
{
|
{
|
||||||
@ -99,13 +102,13 @@ export default function CodeEditor({
|
|||||||
>([])
|
>([])
|
||||||
|
|
||||||
// Preview state
|
// Preview state
|
||||||
const [previewURL, setPreviewURL] = useState<string>("");
|
const [previewURL, setPreviewURL] = useState<string>("")
|
||||||
|
|
||||||
const loadPreviewURL = (url: string) => {
|
const loadPreviewURL = (url: string) => {
|
||||||
// This will cause a reload if previewURL changed.
|
// This will cause a reload if previewURL changed.
|
||||||
setPreviewURL(url);
|
setPreviewURL(url)
|
||||||
// If the URL didn't change, still reload the preview.
|
// If the URL didn't change, still reload the preview.
|
||||||
previewWindowRef.current?.refreshIframe();
|
previewWindowRef.current?.refreshIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOwner = sandboxData.userId === userData.id
|
const isOwner = sandboxData.userId === userData.id
|
||||||
@ -118,23 +121,29 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Liveblocks providers map to prevent reinitializing providers
|
// Liveblocks providers map to prevent reinitializing providers
|
||||||
type ProviderData = {
|
type ProviderData = {
|
||||||
provider: LiveblocksProvider<never, never, never, never>;
|
provider: LiveblocksProvider<never, never, never, never>
|
||||||
yDoc: Y.Doc;
|
yDoc: Y.Doc
|
||||||
yText: Y.Text;
|
yText: Y.Text
|
||||||
binding?: MonacoBinding;
|
binding?: MonacoBinding
|
||||||
onSync: (isSynced: boolean) => void;
|
onSync: (isSynced: boolean) => void
|
||||||
};
|
}
|
||||||
const providersMap = useRef(new Map<string, ProviderData>());
|
const providersMap = useRef(new Map<string, ProviderData>())
|
||||||
|
|
||||||
// Refs for libraries / features
|
// Refs for libraries / features
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const monacoRef = useRef<typeof monaco | null>(null)
|
const monacoRef = useRef<typeof monaco | null>(null)
|
||||||
const generateRef = useRef<HTMLDivElement>(null)
|
const generateRef = useRef<HTMLDivElement>(null)
|
||||||
|
const suggestionRef = useRef<HTMLDivElement>(null)
|
||||||
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
const generateWidgetRef = useRef<HTMLDivElement>(null)
|
||||||
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
const previewPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||||
|
|
||||||
|
const debouncedSetIsSelected = useRef(
|
||||||
|
debounce((value: boolean) => {
|
||||||
|
setIsSelected(value)
|
||||||
|
}, 800) //
|
||||||
|
).current
|
||||||
// Pre-mount editor keybindings
|
// Pre-mount editor keybindings
|
||||||
const handleEditorWillMount: BeforeMount = (monaco) => {
|
const handleEditorWillMount: BeforeMount = (monaco) => {
|
||||||
monaco.editor.addKeybindingRules([
|
monaco.editor.addKeybindingRules([
|
||||||
@ -151,6 +160,13 @@ export default function CodeEditor({
|
|||||||
monacoRef.current = monaco
|
monacoRef.current = monaco
|
||||||
|
|
||||||
editor.onDidChangeCursorPosition((e) => {
|
editor.onDidChangeCursorPosition((e) => {
|
||||||
|
setIsSelected(false)
|
||||||
|
const selection = editor.getSelection()
|
||||||
|
if (selection !== null) {
|
||||||
|
const hasSelection = !selection.isEmpty()
|
||||||
|
debouncedSetIsSelected(hasSelection)
|
||||||
|
setShowSuggestion(hasSelection)
|
||||||
|
}
|
||||||
const { column, lineNumber } = e.position
|
const { column, lineNumber } = e.position
|
||||||
if (lineNumber === cursorLine) return
|
if (lineNumber === cursorLine) return
|
||||||
setCursorLine(lineNumber)
|
setCursorLine(lineNumber)
|
||||||
@ -204,7 +220,44 @@ export default function CodeEditor({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleAiEdit = React.useCallback(() => {
|
||||||
|
if (!editorRef) return
|
||||||
|
const selection = editorRef.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
const pos = selection.getPosition()
|
||||||
|
const start = selection.getStartPosition()
|
||||||
|
const end = selection.getEndPosition()
|
||||||
|
let pref: monaco.editor.ContentWidgetPositionPreference
|
||||||
|
let id = ""
|
||||||
|
const isMultiline = start.lineNumber !== end.lineNumber
|
||||||
|
if (isMultiline) {
|
||||||
|
if (pos.lineNumber <= start.lineNumber) {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
} else {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
}
|
||||||
|
editorRef.changeViewZones(function (changeAccessor) {
|
||||||
|
if (!generateRef.current) return
|
||||||
|
if (pref === monaco.editor.ContentWidgetPositionPreference.ABOVE) {
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber: start.lineNumber - 1,
|
||||||
|
heightInLines: 2,
|
||||||
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setGenerate((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
show: true,
|
||||||
|
pref: [pref],
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [editorRef])
|
||||||
// Generate widget effect
|
// Generate widget effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ai) {
|
if (!ai) {
|
||||||
@ -217,15 +270,21 @@ export default function CodeEditor({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (generate.show) {
|
if (generate.show) {
|
||||||
|
setShowSuggestion(false)
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
if (!generateRef.current) return
|
if (!generateRef.current) return
|
||||||
const id = changeAccessor.addZone({
|
if (!generate.id) {
|
||||||
afterLineNumber: cursorLine,
|
const id = changeAccessor.addZone({
|
||||||
heightInLines: 3,
|
afterLineNumber: cursorLine,
|
||||||
domNode: generateRef.current,
|
heightInLines: 3,
|
||||||
})
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
setGenerate((prev) => {
|
||||||
|
return { ...prev, id, line: cursorLine }
|
||||||
|
})
|
||||||
|
}
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id, line: cursorLine }
|
return { ...prev, line: cursorLine }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -286,6 +345,41 @@ export default function CodeEditor({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [generate.show])
|
}, [generate.show])
|
||||||
|
// Suggestion widget effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (!suggestionRef.current || !editorRef) return
|
||||||
|
const widgetElement = suggestionRef.current
|
||||||
|
const suggestionWidget: monaco.editor.IContentWidget = {
|
||||||
|
getDomNode: () => {
|
||||||
|
return widgetElement
|
||||||
|
},
|
||||||
|
getId: () => {
|
||||||
|
return "suggestion.widget"
|
||||||
|
},
|
||||||
|
getPosition: () => {
|
||||||
|
const selection = editorRef?.getSelection()
|
||||||
|
const column = Math.max(3, selection?.positionColumn ?? 1)
|
||||||
|
let lineNumber = selection?.positionLineNumber ?? 1
|
||||||
|
let pref = monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
if (lineNumber <= 3) {
|
||||||
|
pref = monaco.editor.ContentWidgetPositionPreference.BELOW
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
preference: [pref],
|
||||||
|
position: {
|
||||||
|
lineNumber,
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
editorRef?.addContentWidget(suggestionWidget)
|
||||||
|
editorRef?.applyFontInfo(suggestionRef.current)
|
||||||
|
} else {
|
||||||
|
editorRef?.removeContentWidget(suggestionWidget)
|
||||||
|
}
|
||||||
|
}, [isSelected])
|
||||||
|
|
||||||
// Decorations effect for generate widget tips
|
// Decorations effect for generate widget tips
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -325,27 +419,27 @@ export default function CodeEditor({
|
|||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
tab.id === activeFileId ? { ...tab, saved: true } : tab
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
console.log(`Saving file...${activeFileId}`);
|
console.log(`Saving file...${activeFileId}`)
|
||||||
console.log(`Saving file...${value}`);
|
console.log(`Saving file...${value}`)
|
||||||
socket?.emit("saveFile", activeFileId, value);
|
socket?.emit("saveFile", activeFileId, value)
|
||||||
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
|
||||||
[socket]
|
[socket]
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
debouncedSaveData(editorRef?.getValue(), activeFileId);
|
debouncedSaveData(editorRef?.getValue(), activeFileId)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
document.addEventListener("keydown", down);
|
document.addEventListener("keydown", down)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down);
|
document.removeEventListener("keydown", down)
|
||||||
};
|
}
|
||||||
}, [activeFileId, tabs, debouncedSaveData]);
|
}, [activeFileId, tabs, debouncedSaveData])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -354,13 +448,13 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
if (!editorRef || !tab || !model) return
|
if (!editorRef || !tab || !model) return
|
||||||
|
|
||||||
let providerData: ProviderData;
|
let providerData: ProviderData
|
||||||
|
|
||||||
// When a file is opened for the first time, create a new provider and store in providersMap.
|
// When a file is opened for the first time, create a new provider and store in providersMap.
|
||||||
if (!providersMap.current.has(tab.id)) {
|
if (!providersMap.current.has(tab.id)) {
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc()
|
||||||
const yText = yDoc.getText(tab.id);
|
const yText = yDoc.getText(tab.id)
|
||||||
const yProvider = new LiveblocksProvider(room, yDoc);
|
const yProvider = new LiveblocksProvider(room, yDoc)
|
||||||
|
|
||||||
// Inserts the file content into the editor once when the tab is changed.
|
// Inserts the file content into the editor once when the tab is changed.
|
||||||
const onSync = (isSynced: boolean) => {
|
const onSync = (isSynced: boolean) => {
|
||||||
@ -381,12 +475,11 @@ export default function CodeEditor({
|
|||||||
yProvider.on("sync", onSync)
|
yProvider.on("sync", onSync)
|
||||||
|
|
||||||
// Save the provider to the map.
|
// Save the provider to the map.
|
||||||
providerData = { provider: yProvider, yDoc, yText, onSync };
|
providerData = { provider: yProvider, yDoc, yText, onSync }
|
||||||
providersMap.current.set(tab.id, providerData);
|
providersMap.current.set(tab.id, providerData)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// When a tab is opened that has been open before, reuse the existing provider.
|
// When a tab is opened that has been open before, reuse the existing provider.
|
||||||
providerData = providersMap.current.get(tab.id)!;
|
providerData = providersMap.current.get(tab.id)!
|
||||||
}
|
}
|
||||||
|
|
||||||
const binding = new MonacoBinding(
|
const binding = new MonacoBinding(
|
||||||
@ -394,21 +487,21 @@ export default function CodeEditor({
|
|||||||
model,
|
model,
|
||||||
new Set([editorRef]),
|
new Set([editorRef]),
|
||||||
providerData.provider.awareness as unknown as Awareness
|
providerData.provider.awareness as unknown as Awareness
|
||||||
);
|
)
|
||||||
|
|
||||||
providerData.binding = binding;
|
providerData.binding = binding
|
||||||
setProvider(providerData.provider);
|
setProvider(providerData.provider)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Cleanup logic
|
// Cleanup logic
|
||||||
if (binding) {
|
if (binding) {
|
||||||
binding.destroy();
|
binding.destroy()
|
||||||
}
|
}
|
||||||
if (providerData.binding) {
|
if (providerData.binding) {
|
||||||
providerData.binding = undefined;
|
providerData.binding = undefined
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [room, activeFileContent]);
|
}, [room, activeFileContent])
|
||||||
|
|
||||||
// Added this effect to clean up when the component unmounts
|
// Added this effect to clean up when the component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -416,14 +509,14 @@ export default function CodeEditor({
|
|||||||
// Clean up all providers when the component unmounts
|
// Clean up all providers when the component unmounts
|
||||||
providersMap.current.forEach((data) => {
|
providersMap.current.forEach((data) => {
|
||||||
if (data.binding) {
|
if (data.binding) {
|
||||||
data.binding.destroy();
|
data.binding.destroy()
|
||||||
}
|
}
|
||||||
data.provider.disconnect();
|
data.provider.disconnect()
|
||||||
data.yDoc.destroy();
|
data.yDoc.destroy()
|
||||||
});
|
})
|
||||||
providersMap.current.clear();
|
providersMap.current.clear()
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
// Connection/disconnection effect
|
// Connection/disconnection effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -435,7 +528,7 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Socket event listener effect
|
// Socket event listener effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onConnect = () => { }
|
const onConnect = () => {}
|
||||||
|
|
||||||
const onDisconnect = () => {
|
const onDisconnect = () => {
|
||||||
setTerminals([])
|
setTerminals([])
|
||||||
@ -481,48 +574,55 @@ export default function CodeEditor({
|
|||||||
socket?.off("disableAccess", onDisableAccess)
|
socket?.off("disableAccess", onDisableAccess)
|
||||||
socket?.off("previewURL", loadPreviewURL)
|
socket?.off("previewURL", loadPreviewURL)
|
||||||
}
|
}
|
||||||
}, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
|
}, [
|
||||||
|
socket,
|
||||||
|
terminals,
|
||||||
|
setTerminals,
|
||||||
|
setFiles,
|
||||||
|
toast,
|
||||||
|
setDisableAccess,
|
||||||
|
isOwner,
|
||||||
|
loadPreviewURL,
|
||||||
|
])
|
||||||
|
|
||||||
// Helper functions for tabs:
|
// Helper functions for tabs:
|
||||||
|
|
||||||
// Select file and load content
|
// Select file and load content
|
||||||
|
|
||||||
// Initialize debounced function once
|
// Initialize debounced function once
|
||||||
const fileCache = useRef(new Map());
|
const fileCache = useRef(new Map())
|
||||||
|
|
||||||
// Debounced function to get file content
|
// Debounced function to get file content
|
||||||
const debouncedGetFile =
|
const debouncedGetFile = (tabId: any, callback: any) => {
|
||||||
(tabId: any, callback: any) => {
|
socket?.emit("getFile", tabId, callback)
|
||||||
socket?.emit('getFile', tabId, callback);
|
|
||||||
} // 300ms debounce delay, adjust as needed
|
} // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
const selectFile = (tab: TTab) => {
|
const selectFile = (tab: TTab) => {
|
||||||
|
if (tab.id === activeFileId) return
|
||||||
|
|
||||||
if (tab.id === activeFileId) return;
|
setGenerate((prev) => ({ ...prev, show: false }))
|
||||||
|
|
||||||
setGenerate((prev) => ({ ...prev, show: false }));
|
const exists = tabs.find((t) => t.id === tab.id)
|
||||||
|
|
||||||
const exists = tabs.find((t) => t.id === tab.id);
|
|
||||||
setTabs((prev) => {
|
setTabs((prev) => {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
setActiveFileId(exists.id);
|
setActiveFileId(exists.id)
|
||||||
return prev;
|
return prev
|
||||||
}
|
}
|
||||||
return [...prev, tab];
|
return [...prev, tab]
|
||||||
});
|
})
|
||||||
|
|
||||||
if (fileCache.current.has(tab.id)) {
|
if (fileCache.current.has(tab.id)) {
|
||||||
setActiveFileContent(fileCache.current.get(tab.id));
|
setActiveFileContent(fileCache.current.get(tab.id))
|
||||||
} else {
|
} else {
|
||||||
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
|
debouncedGetFile(tab.id, (response: SetStateAction<string>) => {
|
||||||
fileCache.current.set(tab.id, response);
|
fileCache.current.set(tab.id, response)
|
||||||
setActiveFileContent(response);
|
setActiveFileContent(response)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditorLanguage(processFileType(tab.name));
|
setEditorLanguage(processFileType(tab.name))
|
||||||
setActiveFileId(tab.id);
|
setActiveFileId(tab.id)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Close tab and remove from tabs
|
// Close tab and remove from tabs
|
||||||
const closeTab = (id: string) => {
|
const closeTab = (id: string) => {
|
||||||
@ -538,8 +638,8 @@ export default function CodeEditor({
|
|||||||
? numTabs === 1
|
? numTabs === 1
|
||||||
? null
|
? null
|
||||||
: index < numTabs - 1
|
: index < numTabs - 1
|
||||||
? tabs[index + 1].id
|
? tabs[index + 1].id
|
||||||
: tabs[index - 1].id
|
: tabs[index - 1].id
|
||||||
: activeFileId
|
: activeFileId
|
||||||
|
|
||||||
setTabs((prev) => prev.filter((t) => t.id !== id))
|
setTabs((prev) => prev.filter((t) => t.id !== id))
|
||||||
@ -632,7 +732,7 @@ export default function CodeEditor({
|
|||||||
<DisableAccessModal
|
<DisableAccessModal
|
||||||
message={disableAccess.message}
|
message={disableAccess.message}
|
||||||
open={disableAccess.isDisabled}
|
open={disableAccess.isDisabled}
|
||||||
setOpen={() => { }}
|
setOpen={() => {}}
|
||||||
/>
|
/>
|
||||||
<Loading />
|
<Loading />
|
||||||
</>
|
</>
|
||||||
@ -643,6 +743,23 @@ export default function CodeEditor({
|
|||||||
{/* Copilot DOM elements */}
|
{/* Copilot DOM elements */}
|
||||||
<PreviewProvider>
|
<PreviewProvider>
|
||||||
<div ref={generateRef} />
|
<div ref={generateRef} />
|
||||||
|
<div ref={suggestionRef} className="absolute">
|
||||||
|
<AnimatePresence>
|
||||||
|
{isSelected && ai && showSuggestion && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ ease: "easeOut", duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button size="xs" type="submit" onClick={handleAiEdit}>
|
||||||
|
<Sparkles className="h-3 w-3 mr-1" />
|
||||||
|
Edit Code
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
<div className="z-50 p-1" ref={generateWidgetRef}>
|
<div className="z-50 p-1" ref={generateWidgetRef}>
|
||||||
{generate.show && ai ? (
|
{generate.show && ai ? (
|
||||||
<GenerateInput
|
<GenerateInput
|
||||||
@ -651,22 +768,49 @@ export default function CodeEditor({
|
|||||||
width={generate.width - 90}
|
width={generate.width - 90}
|
||||||
data={{
|
data={{
|
||||||
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
|
||||||
code: editorRef?.getValue() ?? "",
|
code:
|
||||||
|
(isSelected && editorRef?.getSelection()
|
||||||
|
? editorRef
|
||||||
|
?.getModel()
|
||||||
|
?.getValueInRange(editorRef?.getSelection()!)
|
||||||
|
: editorRef?.getValue()) ?? "",
|
||||||
line: generate.line,
|
line: generate.line,
|
||||||
}}
|
}}
|
||||||
editor={{
|
editor={{
|
||||||
language: editorLanguage,
|
language: editorLanguage,
|
||||||
}}
|
}}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
|
const line = generate.line
|
||||||
|
|
||||||
editorRef?.changeViewZones(function (changeAccessor) {
|
editorRef?.changeViewZones(function (changeAccessor) {
|
||||||
changeAccessor.removeZone(generate.id)
|
changeAccessor.removeZone(generate.id)
|
||||||
|
|
||||||
if (!generateRef.current) return
|
if (!generateRef.current) return
|
||||||
const id = changeAccessor.addZone({
|
let id = ""
|
||||||
afterLineNumber: cursorLine,
|
if (isSelected) {
|
||||||
heightInLines: 12,
|
const selection = editorRef?.getSelection()
|
||||||
domNode: generateRef.current,
|
if (!selection) return
|
||||||
})
|
const isAbove =
|
||||||
|
generate.pref?.[0] ===
|
||||||
|
monaco.editor.ContentWidgetPositionPreference.ABOVE
|
||||||
|
const afterLineNumber = isAbove ? line - 1 : line
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber,
|
||||||
|
heightInLines: isAbove?11: 12,
|
||||||
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
const contentWidget= generate.widget
|
||||||
|
if (contentWidget){
|
||||||
|
editorRef?.layoutContentWidget(contentWidget)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id = changeAccessor.addZone({
|
||||||
|
afterLineNumber: cursorLine,
|
||||||
|
heightInLines: 12,
|
||||||
|
|
||||||
|
domNode: generateRef.current,
|
||||||
|
})
|
||||||
|
}
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
return { ...prev, id }
|
return { ...prev, id }
|
||||||
})
|
})
|
||||||
@ -680,12 +824,14 @@ export default function CodeEditor({
|
|||||||
show: !prev.show,
|
show: !prev.show,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const file = editorRef?.getValue()
|
const selection = editorRef?.getSelection()
|
||||||
|
const range =
|
||||||
const lines = file?.split("\n") || []
|
isSelected && selection
|
||||||
lines.splice(line - 1, 0, code)
|
? selection
|
||||||
const updatedFile = lines.join("\n")
|
: new monaco.Range(line, 1, line, 1)
|
||||||
editorRef?.setValue(updatedFile)
|
editorRef?.executeEdits("ai-generation", [
|
||||||
|
{ range, text: code, forceMoveMarkers: true },
|
||||||
|
])
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setGenerate((prev) => {
|
setGenerate((prev) => {
|
||||||
@ -754,58 +900,58 @@ export default function CodeEditor({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||||
clerk.loaded ? (
|
clerk.loaded ? (
|
||||||
<>
|
<>
|
||||||
{provider && userInfo ? (
|
{provider && userInfo ? (
|
||||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||||
) : null}
|
) : null}
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
language={editorLanguage}
|
language={editorLanguage}
|
||||||
beforeMount={handleEditorWillMount}
|
beforeMount={handleEditorWillMount}
|
||||||
onMount={handleEditorMount}
|
onMount={handleEditorMount}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (value === activeFileContent) {
|
if (value === activeFileContent) {
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId
|
tab.id === activeFileId
|
||||||
? { ...tab, saved: true }
|
? { ...tab, saved: true }
|
||||||
: tab
|
: tab
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else {
|
)
|
||||||
setTabs((prev) =>
|
} else {
|
||||||
prev.map((tab) =>
|
setTabs((prev) =>
|
||||||
tab.id === activeFileId
|
prev.map((tab) =>
|
||||||
? { ...tab, saved: false }
|
tab.id === activeFileId
|
||||||
: tab
|
? { ...tab, saved: false }
|
||||||
)
|
: tab
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}}
|
}
|
||||||
options={{
|
}}
|
||||||
tabSize: 2,
|
options={{
|
||||||
minimap: {
|
tabSize: 2,
|
||||||
enabled: false,
|
minimap: {
|
||||||
},
|
enabled: false,
|
||||||
padding: {
|
},
|
||||||
bottom: 4,
|
padding: {
|
||||||
top: 4,
|
bottom: 4,
|
||||||
},
|
top: 4,
|
||||||
scrollBeyondLastLine: false,
|
},
|
||||||
fixedOverflowWidgets: true,
|
scrollBeyondLastLine: false,
|
||||||
fontFamily: "var(--font-geist-mono)",
|
fixedOverflowWidgets: true,
|
||||||
}}
|
fontFamily: "var(--font-geist-mono)",
|
||||||
theme="vs-dark"
|
}}
|
||||||
value={activeFileContent}
|
theme="vs-dark"
|
||||||
/>
|
value={activeFileContent}
|
||||||
</>
|
/>
|
||||||
) : (
|
</>
|
||||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
) : (
|
||||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||||
Waiting for Clerk to load...
|
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||||
</div>
|
Waiting for Clerk to load...
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
@ -825,7 +971,11 @@ export default function CodeEditor({
|
|||||||
open={() => {
|
open={() => {
|
||||||
usePreview().previewPanelRef.current?.expand()
|
usePreview().previewPanelRef.current?.expand()
|
||||||
setIsPreviewCollapsed(false)
|
setIsPreviewCollapsed(false)
|
||||||
}} collapsed={isPreviewCollapsed} src={previewURL} ref={previewWindowRef} />
|
}}
|
||||||
|
collapsed={isPreviewCollapsed}
|
||||||
|
src={previewURL}
|
||||||
|
ref={previewWindowRef}
|
||||||
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
@ -849,4 +999,3 @@ export default function CodeEditor({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user