@tscircuit/fake-snippets 0.0.97 → 0.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +9 -146
- package/dist/bundle.js +1 -2
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -1
- package/package.json +5 -4
- package/src/App.tsx +10 -1
- package/src/components/DownloadButtonAndMenu.tsx +13 -0
- package/src/components/FileSidebar.tsx +83 -10
- package/src/components/PackageBuildsPage/LogContent.tsx +19 -7
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +15 -2
- package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +14 -1
- package/src/components/package-port/CodeAndPreview.tsx +5 -0
- package/src/components/package-port/CodeEditor.tsx +95 -10
- package/src/components/package-port/CodeEditorHeader.tsx +24 -14
- package/src/components/ui/tree-view.tsx +51 -2
- package/src/hooks/use-create-package-release-mutation.ts +2 -2
- package/src/hooks/useFileManagement.ts +70 -1
- package/src/lib/constants.ts +11 -0
- package/src/lib/download-fns/download-spice-file.ts +13 -0
- package/src/lib/utils/resolveRelativePath.ts +40 -0
- package/src/pages/dashboard.tsx +1 -1
- package/src/pages/datasheet.tsx +157 -67
- package/src/pages/datasheets.tsx +2 -2
- package/src/pages/latest.tsx +2 -2
- package/src/pages/search.tsx +1 -1
- package/src/pages/trending.tsx +2 -2
- package/vite.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fake-snippets",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.99",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
|
76
76
|
"@radix-ui/react-tooltip": "^1.1.2",
|
|
77
77
|
"@tailwindcss/typography": "^0.5.16",
|
|
78
|
-
"@tscircuit/3d-viewer": "^0.0.
|
|
78
|
+
"@tscircuit/3d-viewer": "^0.0.303",
|
|
79
79
|
"@tscircuit/assembly-viewer": "^0.0.1",
|
|
80
80
|
"@tscircuit/create-snippet-url": "^0.0.8",
|
|
81
81
|
"@tscircuit/eval": "^0.0.244",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@tscircuit/mm": "^0.0.8",
|
|
84
84
|
"@tscircuit/pcb-viewer": "^1.11.194",
|
|
85
85
|
"@tscircuit/prompt-benchmarks": "^0.0.28",
|
|
86
|
-
"@tscircuit/runframe": "
|
|
86
|
+
"@tscircuit/runframe": "0.0.715",
|
|
87
87
|
"@tscircuit/schematic-viewer": "^2.0.21",
|
|
88
88
|
"@types/babel__standalone": "^7.1.7",
|
|
89
89
|
"@types/bun": "^1.1.10",
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
"wouter": "^3.3.5",
|
|
188
188
|
"zod": "^3.23.8",
|
|
189
189
|
"zustand": "^4.5.5",
|
|
190
|
-
"zustand-hoist": "^2.0.1"
|
|
190
|
+
"zustand-hoist": "^2.0.1",
|
|
191
|
+
"circuit-json-to-spice": "^0.0.6"
|
|
191
192
|
}
|
|
192
193
|
}
|
package/src/App.tsx
CHANGED
|
@@ -122,6 +122,7 @@ class ErrorBoundary extends React.Component<
|
|
|
122
122
|
cleanup = () => {
|
|
123
123
|
if (this.visibilityHandler) {
|
|
124
124
|
document.removeEventListener("visibilitychange", this.visibilityHandler)
|
|
125
|
+
window.removeEventListener("focus", this.visibilityHandler)
|
|
125
126
|
this.visibilityHandler = undefined
|
|
126
127
|
}
|
|
127
128
|
if (this.reloadTimeout) {
|
|
@@ -144,12 +145,20 @@ class ErrorBoundary extends React.Component<
|
|
|
144
145
|
this.cleanup() // Clean up any existing handlers
|
|
145
146
|
|
|
146
147
|
this.visibilityHandler = () => {
|
|
147
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
document.visibilityState === "visible" &&
|
|
150
|
+
this.state.hasError &&
|
|
151
|
+
!this.state.reloading
|
|
152
|
+
) {
|
|
148
153
|
this.performReload()
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
document.addEventListener("visibilitychange", this.visibilityHandler)
|
|
158
|
+
window.addEventListener("focus", this.visibilityHandler)
|
|
159
|
+
|
|
160
|
+
// In case the tab is already visible when the error occurs
|
|
161
|
+
this.visibilityHandler()
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
render() {
|
|
@@ -12,6 +12,7 @@ import { downloadDsnFile } from "@/lib/download-fns/download-dsn-file-fn"
|
|
|
12
12
|
import { downloadFabricationFiles } from "@/lib/download-fns/download-fabrication-files"
|
|
13
13
|
import { downloadSchematicSvg } from "@/lib/download-fns/download-schematic-svg"
|
|
14
14
|
import { downloadReadableNetlist } from "@/lib/download-fns/download-readable-netlist"
|
|
15
|
+
import { downloadSpiceFile } from "@/lib/download-fns/download-spice-file"
|
|
15
16
|
import { downloadAssemblySvg } from "@/lib/download-fns/download-assembly-svg"
|
|
16
17
|
import { usePcbDownloadDialog } from "@/components/dialogs/pcb-download-dialog"
|
|
17
18
|
import { downloadKicadFiles } from "@/lib/download-fns/download-kicad-files"
|
|
@@ -214,6 +215,18 @@ export function DownloadButtonAndMenu({
|
|
|
214
215
|
txt
|
|
215
216
|
</span>
|
|
216
217
|
</DropdownMenuItem>
|
|
218
|
+
<DropdownMenuItem
|
|
219
|
+
className="text-xs"
|
|
220
|
+
onSelect={() => {
|
|
221
|
+
downloadSpiceFile(circuitJson, unscopedName || "circuit")
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<Download className="mr-1 h-3 w-3" />
|
|
225
|
+
<span className="flex-grow mr-6">SPICE Netlist</span>
|
|
226
|
+
<span className="text-[0.6rem] opacity-80 bg-blue-500 text-white font-mono rounded-md px-1 text-center py-0.5 mr-1">
|
|
227
|
+
spice
|
|
228
|
+
</span>
|
|
229
|
+
</DropdownMenuItem>
|
|
217
230
|
<DropdownMenuItem
|
|
218
231
|
className="text-xs"
|
|
219
232
|
onSelect={() => {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import React, { useState } from "react"
|
|
2
2
|
import { cn } from "@/lib/utils"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
File,
|
|
5
|
+
Folder,
|
|
6
|
+
MoreVertical,
|
|
7
|
+
PanelRightOpen,
|
|
8
|
+
Plus,
|
|
9
|
+
Trash2,
|
|
10
|
+
Pencil,
|
|
11
|
+
} from "lucide-react"
|
|
4
12
|
import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
|
|
5
13
|
import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
|
|
6
14
|
import { Input } from "@/components/ui/input"
|
|
@@ -16,8 +24,12 @@ import type {
|
|
|
16
24
|
ICreateFileResult,
|
|
17
25
|
IDeleteFileProps,
|
|
18
26
|
IDeleteFileResult,
|
|
27
|
+
IRenameFileProps,
|
|
28
|
+
IRenameFileResult,
|
|
19
29
|
} from "@/hooks/useFileManagement"
|
|
20
30
|
import { useToast } from "@/hooks/use-toast"
|
|
31
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
32
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
21
33
|
type FileName = string
|
|
22
34
|
|
|
23
35
|
interface FileSidebarProps {
|
|
@@ -28,6 +40,10 @@ interface FileSidebarProps {
|
|
|
28
40
|
fileSidebarState: ReturnType<typeof useState<boolean>>
|
|
29
41
|
handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
|
|
30
42
|
handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
|
|
43
|
+
handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
|
|
44
|
+
isCreatingFile: boolean
|
|
45
|
+
setIsCreatingFile: React.Dispatch<React.SetStateAction<boolean>>
|
|
46
|
+
pkg?: Package
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
@@ -38,12 +54,18 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
38
54
|
fileSidebarState,
|
|
39
55
|
handleCreateFile,
|
|
40
56
|
handleDeleteFile,
|
|
57
|
+
handleRenameFile,
|
|
58
|
+
isCreatingFile,
|
|
59
|
+
setIsCreatingFile,
|
|
60
|
+
pkg,
|
|
41
61
|
}) => {
|
|
42
62
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
43
63
|
const [newFileName, setNewFileName] = useState("")
|
|
44
|
-
const [isCreatingFile, setIsCreatingFile] = useState(false)
|
|
45
64
|
const [errorMessage, setErrorMessage] = useState("")
|
|
65
|
+
const [renamingFile, setRenamingFile] = useState<string | null>(null)
|
|
46
66
|
const { toast } = useToast()
|
|
67
|
+
const session = useGlobalStore((s) => s.session)
|
|
68
|
+
const canModifyFiles = true
|
|
47
69
|
|
|
48
70
|
const transformFilesToTreeData = (
|
|
49
71
|
files: Record<FileName, string>,
|
|
@@ -79,20 +101,59 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
79
101
|
) {
|
|
80
102
|
currentNode[segment] = {
|
|
81
103
|
id: itemId,
|
|
82
|
-
name:
|
|
104
|
+
name: segment,
|
|
105
|
+
isRenaming: renamingFile === itemId,
|
|
106
|
+
onRename: (newFilename: string) => {
|
|
107
|
+
// Preserve the folder structure when renaming
|
|
108
|
+
const oldPath = itemId
|
|
109
|
+
const pathParts = oldPath.split("/").filter((part) => part !== "") // Remove empty segments
|
|
110
|
+
let newPath: string
|
|
111
|
+
|
|
112
|
+
if (pathParts.length > 1) {
|
|
113
|
+
// File is in a folder, preserve the folder structure
|
|
114
|
+
const folderPath = pathParts.slice(0, -1).join("/")
|
|
115
|
+
newPath = folderPath + "/" + newFilename
|
|
116
|
+
} else {
|
|
117
|
+
// File is in root, just use the new filename
|
|
118
|
+
newPath = newFilename
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Preserve leading slash if original path had one
|
|
122
|
+
if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
|
|
123
|
+
newPath = "/" + newPath
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { fileRenamed } = handleRenameFile({
|
|
127
|
+
oldFilename: itemId,
|
|
128
|
+
newFilename: newPath,
|
|
129
|
+
onError: (error) => {
|
|
130
|
+
toast({
|
|
131
|
+
title: `Error renaming file`,
|
|
132
|
+
description: error.message,
|
|
133
|
+
variant: "destructive",
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
if (fileRenamed) {
|
|
138
|
+
setRenamingFile(null)
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
onCancelRename: () => {
|
|
142
|
+
setRenamingFile(null)
|
|
143
|
+
},
|
|
83
144
|
icon: isLeafNode ? File : Folder,
|
|
84
145
|
onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
|
|
85
146
|
draggable: false,
|
|
86
147
|
droppable: !isLeafNode,
|
|
87
148
|
children: isLeafNode ? undefined : {},
|
|
88
|
-
actions: (
|
|
149
|
+
actions: canModifyFiles ? (
|
|
89
150
|
<>
|
|
90
151
|
<DropdownMenu key={itemId}>
|
|
91
152
|
<DropdownMenuTrigger asChild>
|
|
92
153
|
<MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
|
|
93
154
|
</DropdownMenuTrigger>
|
|
94
155
|
<DropdownMenuContent
|
|
95
|
-
className="w-
|
|
156
|
+
className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
|
|
96
157
|
style={{
|
|
97
158
|
position: "absolute",
|
|
98
159
|
top: "100%",
|
|
@@ -103,13 +164,24 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
103
164
|
}}
|
|
104
165
|
>
|
|
105
166
|
<DropdownMenuGroup>
|
|
167
|
+
{isLeafNode && (
|
|
168
|
+
<DropdownMenuItem
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setRenamingFile(itemId)
|
|
171
|
+
}}
|
|
172
|
+
className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
|
|
173
|
+
>
|
|
174
|
+
<Pencil className="mr-2 h-3 w-3" />
|
|
175
|
+
Rename
|
|
176
|
+
</DropdownMenuItem>
|
|
177
|
+
)}
|
|
106
178
|
<DropdownMenuItem
|
|
107
179
|
onClick={() => {
|
|
108
180
|
const { fileDeleted } = handleDeleteFile({
|
|
109
|
-
filename:
|
|
181
|
+
filename: itemId,
|
|
110
182
|
onError: (error) => {
|
|
111
183
|
toast({
|
|
112
|
-
title: `Error deleting file ${
|
|
184
|
+
title: `Error deleting file ${itemId}`,
|
|
113
185
|
description: error.message,
|
|
114
186
|
})
|
|
115
187
|
},
|
|
@@ -118,15 +190,16 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
118
190
|
setErrorMessage("")
|
|
119
191
|
}
|
|
120
192
|
}}
|
|
121
|
-
className="flex items-center px-
|
|
193
|
+
className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
|
|
122
194
|
>
|
|
195
|
+
<Trash2 className="mr-2 h-3 w-3" />
|
|
123
196
|
Delete
|
|
124
197
|
</DropdownMenuItem>
|
|
125
198
|
</DropdownMenuGroup>
|
|
126
199
|
</DropdownMenuContent>
|
|
127
200
|
</DropdownMenu>
|
|
128
201
|
</>
|
|
129
|
-
),
|
|
202
|
+
) : undefined,
|
|
130
203
|
}
|
|
131
204
|
}
|
|
132
205
|
|
|
@@ -152,7 +225,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
152
225
|
}
|
|
153
226
|
|
|
154
227
|
const treeData = transformFilesToTreeData(files)
|
|
155
|
-
|
|
228
|
+
|
|
156
229
|
const handleCreateFileInline = () => {
|
|
157
230
|
const { newFileCreated } = handleCreateFile({
|
|
158
231
|
newFileName,
|
|
@@ -19,34 +19,46 @@ export const LogContent = ({
|
|
|
19
19
|
type?: "info" | "success" | "error"
|
|
20
20
|
msg?: string
|
|
21
21
|
message?: string
|
|
22
|
-
timestamp?: string
|
|
22
|
+
timestamp?: string | number
|
|
23
|
+
[key: string]: unknown
|
|
23
24
|
}>
|
|
24
25
|
error?: ErrorObject | string | null
|
|
25
26
|
}) => {
|
|
26
27
|
return (
|
|
27
28
|
<div className="font-mono text-xs space-y-1 min-w-0">
|
|
28
29
|
{logs.map((log, i) => {
|
|
29
|
-
const
|
|
30
|
+
const { type, msg, message, timestamp, ...rest } = log
|
|
31
|
+
const text = msg ?? message
|
|
30
32
|
if (!text) return null
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
35
|
<div
|
|
34
36
|
key={i}
|
|
35
37
|
className={`break-words whitespace-pre-wrap ${
|
|
36
|
-
|
|
38
|
+
type === "error"
|
|
37
39
|
? "text-red-600"
|
|
38
|
-
:
|
|
40
|
+
: type === "success"
|
|
39
41
|
? "text-green-600"
|
|
40
42
|
: "text-gray-600"
|
|
41
43
|
}`}
|
|
42
44
|
>
|
|
43
|
-
{
|
|
45
|
+
{timestamp !== undefined && (
|
|
44
46
|
<span className="text-gray-500 whitespace-nowrap">
|
|
45
|
-
{new Date(
|
|
47
|
+
{new Date(Number(timestamp)).toLocaleTimeString()}
|
|
46
48
|
</span>
|
|
47
49
|
)}
|
|
48
|
-
{
|
|
50
|
+
{timestamp !== undefined && " "}
|
|
49
51
|
<span className="break-all">{text}</span>
|
|
52
|
+
{Object.keys(rest).filter((k) => k !== "package_release_id")
|
|
53
|
+
.length > 0 && (
|
|
54
|
+
<span className="text-gray-500">
|
|
55
|
+
{" "}
|
|
56
|
+
{Object.entries(rest)
|
|
57
|
+
.filter(([key]) => key !== "package_release_id")
|
|
58
|
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
59
|
+
.join(" ")}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
50
62
|
</div>
|
|
51
63
|
)
|
|
52
64
|
})}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
2
2
|
import { useEffect, useMemo } from "react"
|
|
3
3
|
import { useQuery } from "react-query"
|
|
4
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
5
|
+
|
|
6
|
+
// Pre-randomized array to avoid flickering on re-renders
|
|
7
|
+
const SKELETON_WIDTHS = ["w-2/3", "w-1/4", "w-5/6", "w-1/3", "w-1/2", "w-3/4"]
|
|
4
8
|
|
|
5
9
|
export const ShikiCodeViewer = ({
|
|
6
10
|
code,
|
|
7
11
|
filePath,
|
|
8
|
-
}: {
|
|
12
|
+
}: {
|
|
13
|
+
code: string
|
|
14
|
+
filePath: string
|
|
15
|
+
}) => {
|
|
9
16
|
const { highlighter } = useShikiHighlighter()
|
|
10
17
|
|
|
11
18
|
const html = useMemo(
|
|
@@ -18,7 +25,13 @@ export const ShikiCodeViewer = ({
|
|
|
18
25
|
)
|
|
19
26
|
|
|
20
27
|
if (!html) {
|
|
21
|
-
return
|
|
28
|
+
return (
|
|
29
|
+
<div className="text-sm p-4">
|
|
30
|
+
{SKELETON_WIDTHS.map((w, i) => (
|
|
31
|
+
<Skeleton key={i} className={`h-4 mb-2 ${w}`} />
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CadViewer } from "@tscircuit/runframe"
|
|
2
2
|
import { useCurrentPackageCircuitJson } from "../../hooks/use-current-package-circuit-json"
|
|
3
|
+
import { Suspense } from "react"
|
|
3
4
|
|
|
4
5
|
export default function ThreeDView() {
|
|
5
6
|
const { circuitJson, isLoading, error } = useCurrentPackageCircuitJson()
|
|
@@ -24,7 +25,19 @@ export default function ThreeDView() {
|
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
27
|
<div className="h-[620px]">
|
|
27
|
-
<
|
|
28
|
+
<Suspense
|
|
29
|
+
fallback={
|
|
30
|
+
<div className="flex justify-center items-center h-full">
|
|
31
|
+
<div className="w-48">
|
|
32
|
+
<div className="loading">
|
|
33
|
+
<div className="loading-bar"></div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
<CadViewer clickToInteractEnabled circuitJson={circuitJson} />
|
|
40
|
+
</Suspense>
|
|
28
41
|
</div>
|
|
29
42
|
)
|
|
30
43
|
}
|
|
@@ -16,6 +16,7 @@ import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
|
|
|
16
16
|
import { ManualEditEvent } from "@tscircuit/props"
|
|
17
17
|
import { useFileManagement } from "@/hooks/useFileManagement"
|
|
18
18
|
import { DEFAULT_CODE } from "@/lib/utils/package-utils"
|
|
19
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
19
20
|
|
|
20
21
|
interface Props {
|
|
21
22
|
pkg?: Package
|
|
@@ -38,6 +39,7 @@ export interface CodeAndPreviewState {
|
|
|
38
39
|
|
|
39
40
|
export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
40
41
|
const { toast } = useToast()
|
|
42
|
+
const session = useGlobalStore((s) => s.session)
|
|
41
43
|
const urlParams = useUrlParams()
|
|
42
44
|
|
|
43
45
|
const templateFromUrl = useMemo(
|
|
@@ -75,6 +77,7 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
|
75
77
|
setLocalFiles,
|
|
76
78
|
localFiles,
|
|
77
79
|
initialFiles,
|
|
80
|
+
renameFile,
|
|
78
81
|
} = useFileManagement({
|
|
79
82
|
templateCode: templateFromUrl?.code,
|
|
80
83
|
currentPackage: pkg,
|
|
@@ -208,6 +211,8 @@ export function CodeAndPreview({ pkg, projectUrl }: Props) {
|
|
|
208
211
|
isSaving={isSaving}
|
|
209
212
|
handleCreateFile={createFile}
|
|
210
213
|
handleDeleteFile={deleteFile}
|
|
214
|
+
handleRenameFile={renameFile}
|
|
215
|
+
pkg={pkg}
|
|
211
216
|
currentFile={currentFile}
|
|
212
217
|
onFileSelect={onFileSelect}
|
|
213
218
|
files={localFiles}
|
|
@@ -15,7 +15,10 @@ import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-uti
|
|
|
15
15
|
import type { ATABootstrapConfig } from "@typescript/ata"
|
|
16
16
|
import { setupTypeAcquisition } from "@typescript/ata"
|
|
17
17
|
import { linter } from "@codemirror/lint"
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
TSCI_PACKAGE_PATTERN,
|
|
20
|
+
LOCAL_FILE_IMPORT_PATTERN,
|
|
21
|
+
} from "@/lib/constants"
|
|
19
22
|
import {
|
|
20
23
|
createSystem,
|
|
21
24
|
createVirtualTypeScriptEnvironment,
|
|
@@ -29,10 +32,10 @@ import tsModule from "typescript"
|
|
|
29
32
|
import CodeEditorHeader, {
|
|
30
33
|
FileName,
|
|
31
34
|
} from "@/components/package-port/CodeEditorHeader"
|
|
32
|
-
import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
|
|
33
35
|
import FileSidebar from "../FileSidebar"
|
|
34
36
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
35
37
|
import type { PackageFile } from "@/types/package"
|
|
38
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
36
39
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
37
40
|
import QuickOpen from "./QuickOpen"
|
|
38
41
|
import GlobalFindReplace from "./GlobalFindReplace"
|
|
@@ -41,9 +44,12 @@ import {
|
|
|
41
44
|
ICreateFileResult,
|
|
42
45
|
IDeleteFileProps,
|
|
43
46
|
IDeleteFileResult,
|
|
47
|
+
IRenameFileProps,
|
|
48
|
+
IRenameFileResult,
|
|
44
49
|
} from "@/hooks/useFileManagement"
|
|
45
50
|
import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
46
51
|
import { inlineCopilot } from "codemirror-copilot"
|
|
52
|
+
import { resolveRelativePath } from "@/lib/utils/resolveRelativePath"
|
|
47
53
|
|
|
48
54
|
const defaultImports = `
|
|
49
55
|
import React from "@types/react/jsx-runtime"
|
|
@@ -62,14 +68,18 @@ export const CodeEditor = ({
|
|
|
62
68
|
pkgFilesLoaded,
|
|
63
69
|
currentFile,
|
|
64
70
|
onFileSelect,
|
|
71
|
+
handleRenameFile,
|
|
65
72
|
handleCreateFile,
|
|
66
73
|
handleDeleteFile,
|
|
74
|
+
pkg,
|
|
67
75
|
}: {
|
|
68
76
|
onCodeChange: (code: string, filename?: string) => void
|
|
69
77
|
files: PackageFile[]
|
|
70
78
|
isSaving?: boolean
|
|
71
79
|
handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
|
|
72
80
|
handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
|
|
81
|
+
handleRenameFile: (props: IRenameFileProps) => IRenameFileResult
|
|
82
|
+
pkg?: Package
|
|
73
83
|
readOnly?: boolean
|
|
74
84
|
isStreaming?: boolean
|
|
75
85
|
pkgFilesLoaded?: boolean
|
|
@@ -105,6 +115,9 @@ export const CodeEditor = ({
|
|
|
105
115
|
return files.find((x) => x.path === "index.tsx")?.path || "index.tsx"
|
|
106
116
|
}, [files])
|
|
107
117
|
|
|
118
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
119
|
+
const [isCreatingFile, setIsCreatingFile] = useState(false)
|
|
120
|
+
|
|
108
121
|
// Set current file on component mount
|
|
109
122
|
useEffect(() => {
|
|
110
123
|
if (files.length === 0 || !pkgFilesLoaded || currentFile) return
|
|
@@ -417,11 +430,13 @@ export const CodeEditor = ({
|
|
|
417
430
|
const lineStart = line.from
|
|
418
431
|
const lineEnd = line.to
|
|
419
432
|
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
420
|
-
|
|
433
|
+
|
|
434
|
+
// Check for TSCI package imports
|
|
435
|
+
const packageMatches = Array.from(
|
|
421
436
|
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
422
437
|
)
|
|
423
438
|
|
|
424
|
-
for (const match of
|
|
439
|
+
for (const match of packageMatches) {
|
|
425
440
|
if (match.index !== undefined) {
|
|
426
441
|
const start = lineStart + match.index
|
|
427
442
|
const end = start + match[0].length
|
|
@@ -484,10 +499,12 @@ export const CodeEditor = ({
|
|
|
484
499
|
const lineStart = line.from
|
|
485
500
|
const lineEnd = line.to
|
|
486
501
|
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
487
|
-
|
|
502
|
+
|
|
503
|
+
// Check for TSCI package imports first
|
|
504
|
+
const packageMatches = Array.from(
|
|
488
505
|
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
489
506
|
)
|
|
490
|
-
for (const match of
|
|
507
|
+
for (const match of packageMatches) {
|
|
491
508
|
if (match.index !== undefined) {
|
|
492
509
|
const start = lineStart + match.index
|
|
493
510
|
const end = start + match[0].length
|
|
@@ -502,6 +519,42 @@ export const CodeEditor = ({
|
|
|
502
519
|
}
|
|
503
520
|
}
|
|
504
521
|
}
|
|
522
|
+
|
|
523
|
+
// Check for local file imports
|
|
524
|
+
const localFileMatches = Array.from(
|
|
525
|
+
lineText.matchAll(LOCAL_FILE_IMPORT_PATTERN),
|
|
526
|
+
)
|
|
527
|
+
for (const match of localFileMatches) {
|
|
528
|
+
if (match.index !== undefined) {
|
|
529
|
+
const start = lineStart + match.index
|
|
530
|
+
const end = start + match[0].length
|
|
531
|
+
if (pos >= start && pos <= end) {
|
|
532
|
+
const relativePath = match[0]
|
|
533
|
+
const resolvedPath = resolveRelativePath(
|
|
534
|
+
relativePath,
|
|
535
|
+
currentFile || "",
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
// Add common extensions if not present
|
|
539
|
+
let targetPath = resolvedPath
|
|
540
|
+
if (!targetPath.includes(".")) {
|
|
541
|
+
const extensions = [".tsx", ".ts", ".js", ".jsx"]
|
|
542
|
+
for (const ext of extensions) {
|
|
543
|
+
if (fileMap[`${targetPath}${ext}`]) {
|
|
544
|
+
targetPath = `${targetPath}${ext}`
|
|
545
|
+
break
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (fileMap[targetPath]) {
|
|
551
|
+
onFileSelect(targetPath)
|
|
552
|
+
return true
|
|
553
|
+
}
|
|
554
|
+
return !!fileMap[targetPath]
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
505
558
|
return false
|
|
506
559
|
},
|
|
507
560
|
keydown: (event) => {
|
|
@@ -529,6 +582,12 @@ export const CodeEditor = ({
|
|
|
529
582
|
overflow: "auto",
|
|
530
583
|
zIndex: "9999",
|
|
531
584
|
},
|
|
585
|
+
".cm-import:hover": {
|
|
586
|
+
textDecoration: "underline",
|
|
587
|
+
textDecorationColor: "#aa1111",
|
|
588
|
+
textUnderlineOffset: "1px",
|
|
589
|
+
filter: "brightness(0.7)",
|
|
590
|
+
},
|
|
532
591
|
}),
|
|
533
592
|
EditorView.decorations.of((view) => {
|
|
534
593
|
const decorations = []
|
|
@@ -536,14 +595,32 @@ export const CodeEditor = ({
|
|
|
536
595
|
for (let pos = from; pos < to; ) {
|
|
537
596
|
const line = view.state.doc.lineAt(pos)
|
|
538
597
|
const lineText = line.text
|
|
539
|
-
|
|
540
|
-
|
|
598
|
+
|
|
599
|
+
// Add decorations for TSCI package imports
|
|
600
|
+
const packageMatches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
|
|
601
|
+
for (const match of packageMatches) {
|
|
541
602
|
if (match.index !== undefined) {
|
|
542
603
|
const start = line.from + match.index
|
|
543
604
|
const end = start + match[0].length
|
|
544
605
|
decorations.push(
|
|
545
606
|
Decoration.mark({
|
|
546
|
-
class: "cm-
|
|
607
|
+
class: "cm-import cursor-pointer",
|
|
608
|
+
}).range(start, end),
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Add decorations for local file imports
|
|
614
|
+
const localFileMatches = lineText.matchAll(
|
|
615
|
+
LOCAL_FILE_IMPORT_PATTERN,
|
|
616
|
+
)
|
|
617
|
+
for (const match of localFileMatches) {
|
|
618
|
+
if (match.index !== undefined) {
|
|
619
|
+
const start = line.from + match.index
|
|
620
|
+
const end = start + match[0].length
|
|
621
|
+
decorations.push(
|
|
622
|
+
Decoration.mark({
|
|
623
|
+
class: "cm-import cursor-pointer",
|
|
547
624
|
}).range(start, end),
|
|
548
625
|
)
|
|
549
626
|
}
|
|
@@ -719,10 +796,14 @@ export const CodeEditor = ({
|
|
|
719
796
|
}
|
|
720
797
|
})
|
|
721
798
|
|
|
799
|
+
useHotkeyCombo("cmd+m", () => {
|
|
800
|
+
setSidebarOpen(true)
|
|
801
|
+
setIsCreatingFile(true)
|
|
802
|
+
})
|
|
803
|
+
|
|
722
804
|
if (isStreaming) {
|
|
723
805
|
return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
|
|
724
806
|
}
|
|
725
|
-
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
726
807
|
return (
|
|
727
808
|
<div className="flex h-[98vh] w-full overflow-hidden">
|
|
728
809
|
<FileSidebar
|
|
@@ -733,7 +814,11 @@ export const CodeEditor = ({
|
|
|
733
814
|
}
|
|
734
815
|
onFileSelect={(path) => handleFileChange(path)}
|
|
735
816
|
handleCreateFile={handleCreateFile}
|
|
817
|
+
handleRenameFile={handleRenameFile}
|
|
736
818
|
handleDeleteFile={handleDeleteFile}
|
|
819
|
+
isCreatingFile={isCreatingFile}
|
|
820
|
+
setIsCreatingFile={setIsCreatingFile}
|
|
821
|
+
pkg={pkg}
|
|
737
822
|
/>
|
|
738
823
|
<div className="flex flex-col flex-1 w-full min-w-0 h-full">
|
|
739
824
|
{showImportAndFormatButtons && (
|