@tscircuit/fake-snippets 0.0.42 → 0.0.44
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 +335 -15
- package/dist/bundle.js +13 -7
- package/fake-snippets-api/routes/api/package_files/create_or_update.ts +9 -0
- package/fake-snippets-api/routes/api/packages/update.ts +1 -0
- package/package.json +5 -5
- package/src/App.tsx +8 -0
- package/src/components/CodeEditor.tsx +5 -3
- package/src/components/CodeEditorHeader.tsx +1 -1
- package/src/components/DownloadButtonAndMenu.tsx +3 -2
- package/src/components/EditorNav.tsx +3 -3
- package/src/components/FileSidebar.tsx +84 -0
- package/src/components/ViewPackagePage/components/important-files-view.tsx +6 -1
- package/src/components/ViewPackagePage/components/package-header.tsx +39 -5
- package/src/components/dialogs/rename-package-dialog.tsx +81 -0
- package/src/components/dialogs/update-package-description-dialog.tsx +96 -0
- package/src/components/package-port/CodeAndPreview.tsx +417 -0
- package/src/components/package-port/CodeEditor.tsx +510 -0
- package/src/components/package-port/CodeEditorHeader.tsx +153 -0
- package/src/components/package-port/EditorNav.tsx +518 -0
- package/src/components/ui/tree-view.tsx +490 -0
- package/src/hooks/use-fork-package-mutation.ts +7 -0
- package/src/hooks/use-package.ts +23 -0
- package/src/hooks/useForkPackageMutation.ts +49 -0
- package/src/hooks/usePackageFilesLoader.ts +56 -0
- package/src/hooks/useUpdatePackageFilesMutation.ts +86 -0
- package/src/hooks/useUpdatePackageMutation.ts +63 -0
- package/src/lib/utils/checkIfManualEditsImported.ts +1 -1
- package/src/pages/package-editor.tsx +44 -0
- /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/create.ts +0 -0
- /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/create_all_vendor_quotes.ts +0 -0
- /package/fake-snippets-api/routes/api/{order_quote → order_quotes}/get.ts +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
|
|
2
|
+
import { basicSetup } from "@/lib/codemirror/basic-setup"
|
|
3
|
+
import { autocompletion } from "@codemirror/autocomplete"
|
|
4
|
+
import { indentWithTab } from "@codemirror/commands"
|
|
5
|
+
import { javascript } from "@codemirror/lang-javascript"
|
|
6
|
+
import { json } from "@codemirror/lang-json"
|
|
7
|
+
import { EditorState } from "@codemirror/state"
|
|
8
|
+
import { Decoration, hoverTooltip, keymap } from "@codemirror/view"
|
|
9
|
+
import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
|
|
10
|
+
import type { ATABootstrapConfig } from "@typescript/ata"
|
|
11
|
+
import { setupTypeAcquisition } from "@typescript/ata"
|
|
12
|
+
import { TSCI_PACKAGE_PATTERN } from "@/lib/constants"
|
|
13
|
+
import {
|
|
14
|
+
createDefaultMapFromCDN,
|
|
15
|
+
createSystem,
|
|
16
|
+
createVirtualTypeScriptEnvironment,
|
|
17
|
+
} from "@typescript/vfs"
|
|
18
|
+
import {
|
|
19
|
+
tsAutocomplete,
|
|
20
|
+
tsFacet,
|
|
21
|
+
tsHover,
|
|
22
|
+
tsLinter,
|
|
23
|
+
tsSync,
|
|
24
|
+
} from "@valtown/codemirror-ts"
|
|
25
|
+
import { EditorView } from "codemirror"
|
|
26
|
+
import { useEffect, useMemo, useRef, useState } from "react"
|
|
27
|
+
import ts from "typescript"
|
|
28
|
+
import CodeEditorHeader from "@/components/package-port/CodeEditorHeader"
|
|
29
|
+
// import { copilotPlugin, Language } from "@valtown/codemirror-codeium"
|
|
30
|
+
import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
|
|
31
|
+
import FileSidebar from "../FileSidebar"
|
|
32
|
+
|
|
33
|
+
const defaultImports = `
|
|
34
|
+
import React from "@types/react/jsx-runtime"
|
|
35
|
+
import { Circuit, createUseComponent } from "@tscircuit/core"
|
|
36
|
+
import type { CommonLayoutProps } from "@tscircuit/props"
|
|
37
|
+
`
|
|
38
|
+
export interface FileContent {
|
|
39
|
+
path: string
|
|
40
|
+
content: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const CodeEditor = ({
|
|
44
|
+
onCodeChange,
|
|
45
|
+
onDtsChange,
|
|
46
|
+
readOnly = false,
|
|
47
|
+
files = [],
|
|
48
|
+
isStreaming = false,
|
|
49
|
+
showImportAndFormatButtons = true,
|
|
50
|
+
onFileContentChanged,
|
|
51
|
+
}: {
|
|
52
|
+
onCodeChange: (code: string, filename?: string) => void
|
|
53
|
+
onDtsChange?: (dts: string) => void
|
|
54
|
+
files: FileContent[]
|
|
55
|
+
readOnly?: boolean
|
|
56
|
+
isStreaming?: boolean
|
|
57
|
+
showImportAndFormatButtons?: boolean
|
|
58
|
+
onFileContentChanged?: (path: string, content: string) => void
|
|
59
|
+
}) => {
|
|
60
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
61
|
+
const viewRef = useRef<EditorView | null>(null)
|
|
62
|
+
const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
|
|
63
|
+
const apiUrl = useSnippetsBaseApiUrl()
|
|
64
|
+
const codeCompletionApi = useCodeCompletionApi()
|
|
65
|
+
|
|
66
|
+
const [cursorPosition, setCursorPosition] = useState<number | null>(null)
|
|
67
|
+
const [code, setCode] = useState(files[0]?.content || "")
|
|
68
|
+
const [currentFile, setCurrentFile] = useState<string>("")
|
|
69
|
+
|
|
70
|
+
// Get URL search params for file_path
|
|
71
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
72
|
+
const filePathFromUrl = urlParams.get("file_path")
|
|
73
|
+
|
|
74
|
+
// Set current file on component mount
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (files.length > 0 && currentFile === "") {
|
|
77
|
+
// Priority 1: Use file_path from URL if it exists in files
|
|
78
|
+
if (
|
|
79
|
+
filePathFromUrl &&
|
|
80
|
+
files.some((file) => file.path === filePathFromUrl)
|
|
81
|
+
) {
|
|
82
|
+
setCurrentFile(filePathFromUrl)
|
|
83
|
+
}
|
|
84
|
+
// Priority 2: Use index.tsx if it exists in files
|
|
85
|
+
else if (files.some((file) => file.path === "index.tsx")) {
|
|
86
|
+
setCurrentFile("index.tsx")
|
|
87
|
+
}
|
|
88
|
+
// Priority 3: Use the first file with .tsx extension
|
|
89
|
+
else {
|
|
90
|
+
const tsxFile = files.find((file) => file.path.endsWith(".tsx"))
|
|
91
|
+
if (tsxFile) {
|
|
92
|
+
setCurrentFile(tsxFile.path)
|
|
93
|
+
}
|
|
94
|
+
// Fallback: Use the first file in the array
|
|
95
|
+
else if (files[0]) {
|
|
96
|
+
setCurrentFile(files[0].path)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
}, [files])
|
|
102
|
+
|
|
103
|
+
const fileMap = useMemo(() => {
|
|
104
|
+
const map: Record<string, string> = {}
|
|
105
|
+
files.forEach((file) => {
|
|
106
|
+
map[file.path] = file.content
|
|
107
|
+
})
|
|
108
|
+
return map
|
|
109
|
+
}, [files])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const currentFileContent =
|
|
113
|
+
files.find((f) => f.path === currentFile)?.content || ""
|
|
114
|
+
if (currentFileContent !== code) {
|
|
115
|
+
setCode(currentFileContent)
|
|
116
|
+
updateCurrentEditorContent(currentFileContent)
|
|
117
|
+
}
|
|
118
|
+
}, [files])
|
|
119
|
+
|
|
120
|
+
// Whenever streaming completes, reset the code to the initial code
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isStreaming) {
|
|
123
|
+
const currentFileContent =
|
|
124
|
+
files.find((f) => f.path === currentFile)?.content || ""
|
|
125
|
+
if (code !== currentFileContent && currentFileContent) {
|
|
126
|
+
setCode(currentFileContent)
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
updateCurrentEditorContent(currentFileContent)
|
|
129
|
+
}, 200)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, [isStreaming])
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!editorRef.current) return
|
|
136
|
+
|
|
137
|
+
const fsMap = new Map<string, string>()
|
|
138
|
+
files.forEach(({ path, content }) => {
|
|
139
|
+
fsMap.set(path, content)
|
|
140
|
+
})
|
|
141
|
+
;(window as any).__DEBUG_CODE_EDITOR_FS_MAP = fsMap
|
|
142
|
+
|
|
143
|
+
createDefaultMapFromCDN(
|
|
144
|
+
{ target: ts.ScriptTarget.ES2022 },
|
|
145
|
+
"5.6.3",
|
|
146
|
+
true,
|
|
147
|
+
ts,
|
|
148
|
+
).then((defaultFsMap) => {
|
|
149
|
+
defaultFsMap.forEach((content, filename) => {
|
|
150
|
+
fsMap.set(filename, content)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const system = createSystem(fsMap)
|
|
155
|
+
const env = createVirtualTypeScriptEnvironment(system, [], ts, {
|
|
156
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
157
|
+
declaration: true,
|
|
158
|
+
allowJs: true,
|
|
159
|
+
target: ts.ScriptTarget.ES2022,
|
|
160
|
+
resolveJsonModule: true,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Initialize ATA
|
|
164
|
+
const ataConfig: ATABootstrapConfig = {
|
|
165
|
+
projectName: "my-project",
|
|
166
|
+
typescript: ts,
|
|
167
|
+
logger: console,
|
|
168
|
+
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
169
|
+
const registryPrefixes = [
|
|
170
|
+
"https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
|
|
171
|
+
"https://data.jsdelivr.com/v1/package/npm/@tsci/",
|
|
172
|
+
"https://cdn.jsdelivr.net/npm/@tsci/",
|
|
173
|
+
]
|
|
174
|
+
if (
|
|
175
|
+
typeof input === "string" &&
|
|
176
|
+
registryPrefixes.some((prefix) => input.startsWith(prefix))
|
|
177
|
+
) {
|
|
178
|
+
const fullPackageName = input
|
|
179
|
+
.replace(registryPrefixes[0], "")
|
|
180
|
+
.replace(registryPrefixes[1], "")
|
|
181
|
+
.replace(registryPrefixes[2], "")
|
|
182
|
+
const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
|
|
183
|
+
const pathInPackage = fullPackageName.split("/").slice(1).join("/")
|
|
184
|
+
const jsdelivrPath = `${packageName}${
|
|
185
|
+
pathInPackage ? `/${pathInPackage}` : ""
|
|
186
|
+
}`
|
|
187
|
+
return fetch(
|
|
188
|
+
`${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes(
|
|
189
|
+
"/resolve/",
|
|
190
|
+
)}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
return fetch(input, init)
|
|
194
|
+
},
|
|
195
|
+
delegate: {
|
|
196
|
+
started: () => {
|
|
197
|
+
const manualEditsTypeDeclaration = `
|
|
198
|
+
declare module "*.json" {
|
|
199
|
+
const value: {
|
|
200
|
+
pcb_placements?: any[],
|
|
201
|
+
schematic_placements?: any[],
|
|
202
|
+
edit_events?: any[],
|
|
203
|
+
manual_trace_hints?: any[],
|
|
204
|
+
} | undefined;
|
|
205
|
+
export default value;
|
|
206
|
+
}
|
|
207
|
+
`
|
|
208
|
+
env.createFile("manual-edits.d.ts", manualEditsTypeDeclaration)
|
|
209
|
+
},
|
|
210
|
+
receivedFile: (code: string, path: string) => {
|
|
211
|
+
fsMap.set(path, code)
|
|
212
|
+
env.createFile(path, code)
|
|
213
|
+
if (viewRef.current) {
|
|
214
|
+
viewRef.current.dispatch({
|
|
215
|
+
changes: {
|
|
216
|
+
from: 0,
|
|
217
|
+
to: viewRef.current.state.doc.length,
|
|
218
|
+
insert: viewRef.current.state.doc.toString(),
|
|
219
|
+
},
|
|
220
|
+
selection: viewRef.current.state.selection,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const ata = setupTypeAcquisition(ataConfig)
|
|
228
|
+
ataRef.current = ata
|
|
229
|
+
|
|
230
|
+
const lastFilesEventContent: Record<string, string> = {}
|
|
231
|
+
|
|
232
|
+
// Set up base extensions
|
|
233
|
+
const baseExtensions = [
|
|
234
|
+
basicSetup,
|
|
235
|
+
currentFile.endsWith(".json")
|
|
236
|
+
? json()
|
|
237
|
+
: javascript({ typescript: true, jsx: true }),
|
|
238
|
+
keymap.of([indentWithTab]),
|
|
239
|
+
EditorState.readOnly.of(readOnly),
|
|
240
|
+
EditorView.updateListener.of((update) => {
|
|
241
|
+
if (update.docChanged) {
|
|
242
|
+
const newContent = update.state.doc.toString()
|
|
243
|
+
|
|
244
|
+
if (newContent === lastFilesEventContent[currentFile]) return
|
|
245
|
+
lastFilesEventContent[currentFile] = newContent
|
|
246
|
+
|
|
247
|
+
// setCode(newContent)
|
|
248
|
+
onCodeChange(newContent, currentFile)
|
|
249
|
+
onFileContentChanged?.(currentFile, newContent)
|
|
250
|
+
|
|
251
|
+
// Generate TypeScript declarations for TypeScript/TSX files
|
|
252
|
+
if (currentFile.endsWith(".ts") || currentFile.endsWith(".tsx")) {
|
|
253
|
+
const { outputFiles } = env.languageService.getEmitOutput(
|
|
254
|
+
currentFile,
|
|
255
|
+
true,
|
|
256
|
+
)
|
|
257
|
+
const dtsFile = outputFiles.find((file) =>
|
|
258
|
+
file.name.endsWith(".d.ts"),
|
|
259
|
+
)
|
|
260
|
+
if (dtsFile?.text && onDtsChange) {
|
|
261
|
+
onDtsChange(dtsFile.text)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (update.selectionSet) {
|
|
266
|
+
const pos = update.state.selection.main.head
|
|
267
|
+
setCursorPosition(pos)
|
|
268
|
+
}
|
|
269
|
+
}),
|
|
270
|
+
]
|
|
271
|
+
if (codeCompletionApi?.apiKey) {
|
|
272
|
+
baseExtensions.push(
|
|
273
|
+
// copilotPlugin({
|
|
274
|
+
// apiKey: codeCompletionApi.apiKey,
|
|
275
|
+
// language: Language.TYPESCRIPT,
|
|
276
|
+
// }),
|
|
277
|
+
EditorView.theme({
|
|
278
|
+
".cm-ghostText, .cm-ghostText *": {
|
|
279
|
+
opacity: "0.6",
|
|
280
|
+
filter: "grayscale(20%)",
|
|
281
|
+
cursor: "pointer",
|
|
282
|
+
},
|
|
283
|
+
".cm-ghostText:hover": {
|
|
284
|
+
background: "#eee",
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Add TypeScript-specific extensions and handlers
|
|
291
|
+
const tsExtensions =
|
|
292
|
+
currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")
|
|
293
|
+
? [
|
|
294
|
+
tsFacet.of({ env, path: currentFile }),
|
|
295
|
+
tsSync(),
|
|
296
|
+
tsLinter(),
|
|
297
|
+
autocompletion({ override: [tsAutocomplete()] }),
|
|
298
|
+
tsHover(),
|
|
299
|
+
hoverTooltip((view, pos) => {
|
|
300
|
+
const line = view.state.doc.lineAt(pos)
|
|
301
|
+
const lineStart = line.from
|
|
302
|
+
const lineEnd = line.to
|
|
303
|
+
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
304
|
+
const matches = Array.from(
|
|
305
|
+
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
for (const match of matches) {
|
|
309
|
+
if (match.index !== undefined) {
|
|
310
|
+
const start = lineStart + match.index
|
|
311
|
+
const end = start + match[0].length
|
|
312
|
+
if (pos >= start && pos <= end) {
|
|
313
|
+
return {
|
|
314
|
+
pos: start,
|
|
315
|
+
end: end,
|
|
316
|
+
above: true,
|
|
317
|
+
create() {
|
|
318
|
+
const dom = document.createElement("div")
|
|
319
|
+
dom.textContent = "Ctrl/Cmd+Click to open snippet"
|
|
320
|
+
return { dom }
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null
|
|
327
|
+
}),
|
|
328
|
+
EditorView.domEventHandlers({
|
|
329
|
+
click: (event, view) => {
|
|
330
|
+
if (!event.ctrlKey && !event.metaKey) return false
|
|
331
|
+
const pos = view.posAtCoords({
|
|
332
|
+
x: event.clientX,
|
|
333
|
+
y: event.clientY,
|
|
334
|
+
})
|
|
335
|
+
if (pos === null) return false
|
|
336
|
+
|
|
337
|
+
const line = view.state.doc.lineAt(pos)
|
|
338
|
+
const lineStart = line.from
|
|
339
|
+
const lineEnd = line.to
|
|
340
|
+
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
341
|
+
const matches = Array.from(
|
|
342
|
+
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
343
|
+
)
|
|
344
|
+
for (const match of matches) {
|
|
345
|
+
if (match.index !== undefined) {
|
|
346
|
+
const start = lineStart + match.index
|
|
347
|
+
const end = start + match[0].length
|
|
348
|
+
if (pos >= start && pos <= end) {
|
|
349
|
+
const importName = match[0]
|
|
350
|
+
// Handle potential dots and dashes in package names
|
|
351
|
+
const [owner, name] = importName
|
|
352
|
+
.replace("@tsci/", "")
|
|
353
|
+
.split(".")
|
|
354
|
+
window.open(`/${owner}/${name}`, "_blank")
|
|
355
|
+
return true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return false
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
EditorView.theme({
|
|
363
|
+
".cm-content .cm-underline": {
|
|
364
|
+
textDecoration: "underline",
|
|
365
|
+
textDecorationColor: "rgba(0, 0, 255, 0.3)",
|
|
366
|
+
cursor: "pointer",
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
EditorView.decorations.of((view) => {
|
|
370
|
+
const decorations = []
|
|
371
|
+
for (const { from, to } of view.visibleRanges) {
|
|
372
|
+
for (let pos = from; pos < to; ) {
|
|
373
|
+
const line = view.state.doc.lineAt(pos)
|
|
374
|
+
const lineText = line.text
|
|
375
|
+
const matches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
|
|
376
|
+
for (const match of matches) {
|
|
377
|
+
if (match.index !== undefined) {
|
|
378
|
+
const start = line.from + match.index
|
|
379
|
+
const end = start + match[0].length
|
|
380
|
+
decorations.push(
|
|
381
|
+
Decoration.mark({
|
|
382
|
+
class: "cm-underline",
|
|
383
|
+
}).range(start, end),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
pos = line.to + 1
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return Decoration.set(decorations)
|
|
391
|
+
}),
|
|
392
|
+
]
|
|
393
|
+
: []
|
|
394
|
+
|
|
395
|
+
const state = EditorState.create({
|
|
396
|
+
doc: fileMap[currentFile] || "",
|
|
397
|
+
extensions: [...baseExtensions, ...tsExtensions],
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const view = new EditorView({
|
|
401
|
+
state,
|
|
402
|
+
parent: editorRef.current,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
viewRef.current = view
|
|
406
|
+
|
|
407
|
+
// Initial ATA run for index.tsx
|
|
408
|
+
if (currentFile === "index.tsx") {
|
|
409
|
+
ata(`${defaultImports}${code}`)
|
|
410
|
+
}
|
|
411
|
+
// files.forEach(({path, content}) => {
|
|
412
|
+
// if (path.endsWith(".tsx") || path.endsWith(".ts")) {
|
|
413
|
+
// ata(`${defaultImports}${content}`)
|
|
414
|
+
// }
|
|
415
|
+
// })
|
|
416
|
+
|
|
417
|
+
return () => {
|
|
418
|
+
view.destroy()
|
|
419
|
+
}
|
|
420
|
+
}, [!isStreaming, currentFile, code !== ""])
|
|
421
|
+
|
|
422
|
+
const updateCurrentEditorContent = (newContent: string) => {
|
|
423
|
+
if (viewRef.current) {
|
|
424
|
+
const state = viewRef.current.state
|
|
425
|
+
if (state.doc.toString() !== newContent) {
|
|
426
|
+
viewRef.current.dispatch({
|
|
427
|
+
changes: { from: 0, to: state.doc.length, insert: newContent },
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const updateEditorToMatchCurrentFile = () => {
|
|
434
|
+
const currentContent = fileMap[currentFile] || ""
|
|
435
|
+
updateCurrentEditorContent(currentContent)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const codeImports = getImportsFromCode(code)
|
|
439
|
+
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (
|
|
442
|
+
ataRef.current &&
|
|
443
|
+
(currentFile.endsWith(".tsx") || currentFile.endsWith(".ts"))
|
|
444
|
+
) {
|
|
445
|
+
ataRef.current(`${defaultImports}${code}`)
|
|
446
|
+
}
|
|
447
|
+
}, [codeImports])
|
|
448
|
+
|
|
449
|
+
const handleFileChange = (path: string) => {
|
|
450
|
+
setCurrentFile(path)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const updateFileContent = (path: string, newContent: string) => {
|
|
454
|
+
if (currentFile === path) {
|
|
455
|
+
setCode(newContent)
|
|
456
|
+
onCodeChange(newContent, path)
|
|
457
|
+
}
|
|
458
|
+
onFileContentChanged?.(path, newContent)
|
|
459
|
+
|
|
460
|
+
if (viewRef.current && currentFile === path) {
|
|
461
|
+
viewRef.current.dispatch({
|
|
462
|
+
changes: {
|
|
463
|
+
from: 0,
|
|
464
|
+
to: viewRef.current.state.doc.length,
|
|
465
|
+
insert: newContent,
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Whenever the current file changes, updated the editor content
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
updateEditorToMatchCurrentFile()
|
|
474
|
+
}, [currentFile])
|
|
475
|
+
|
|
476
|
+
if (isStreaming) {
|
|
477
|
+
return <div className="font-mono whitespace-pre-wrap text-xs">{code}</div>
|
|
478
|
+
}
|
|
479
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
480
|
+
return (
|
|
481
|
+
<div className="flex h-full">
|
|
482
|
+
<FileSidebar
|
|
483
|
+
files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
|
|
484
|
+
currentFile={currentFile}
|
|
485
|
+
fileSidebarState={
|
|
486
|
+
[sidebarOpen, setSidebarOpen] as ReturnType<typeof useState<boolean>>
|
|
487
|
+
}
|
|
488
|
+
onFileSelect={handleFileChange}
|
|
489
|
+
/>
|
|
490
|
+
<div className="flex flex-col flex-1">
|
|
491
|
+
{showImportAndFormatButtons && (
|
|
492
|
+
<CodeEditorHeader
|
|
493
|
+
fileSidebarState={
|
|
494
|
+
[sidebarOpen, setSidebarOpen] as ReturnType<
|
|
495
|
+
typeof useState<boolean>
|
|
496
|
+
>
|
|
497
|
+
}
|
|
498
|
+
currentFile={currentFile}
|
|
499
|
+
files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
|
|
500
|
+
updateFileContent={(...args) => {
|
|
501
|
+
return updateFileContent(...args)
|
|
502
|
+
}}
|
|
503
|
+
cursorPosition={cursorPosition}
|
|
504
|
+
/>
|
|
505
|
+
)}
|
|
506
|
+
<div ref={editorRef} className="flex-1 overflow-auto max-w-[100%]" />
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
)
|
|
510
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useState, useCallback } from "react"
|
|
2
|
+
import { Button } from "@/components/ui/button"
|
|
3
|
+
import { handleManualEditsImport } from "@/lib/handleManualEditsImport"
|
|
4
|
+
import { useImportSnippetDialog } from "@/components/dialogs/import-snippet-dialog"
|
|
5
|
+
import { useToast } from "@/hooks/use-toast"
|
|
6
|
+
import { FootprintDialog } from "@/components/FootprintDialog"
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "@/components/ui/dropdown-menu"
|
|
13
|
+
import { AlertTriangle, PanelRightClose } from "lucide-react"
|
|
14
|
+
import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
|
|
15
|
+
|
|
16
|
+
export type FileName = string
|
|
17
|
+
|
|
18
|
+
interface CodeEditorHeaderProps {
|
|
19
|
+
currentFile: FileName
|
|
20
|
+
files: Record<FileName, string>
|
|
21
|
+
updateFileContent: (filename: FileName, content: string) => void
|
|
22
|
+
cursorPosition: number | null
|
|
23
|
+
fileSidebarState: ReturnType<typeof useState<boolean>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
27
|
+
currentFile,
|
|
28
|
+
files,
|
|
29
|
+
updateFileContent,
|
|
30
|
+
cursorPosition,
|
|
31
|
+
fileSidebarState,
|
|
32
|
+
}) => {
|
|
33
|
+
const { Dialog: ImportSnippetDialog, openDialog: openImportDialog } =
|
|
34
|
+
useImportSnippetDialog()
|
|
35
|
+
const [footprintDialogOpen, setFootprintDialogOpen] = useState(false)
|
|
36
|
+
const { toast } = useToast()
|
|
37
|
+
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
38
|
+
const handleFormatFile = useCallback(() => {
|
|
39
|
+
if (!window.prettier || !window.prettierPlugins) return
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const currentContent = files[currentFile]
|
|
43
|
+
|
|
44
|
+
if (currentFile.endsWith(".json")) {
|
|
45
|
+
try {
|
|
46
|
+
const jsonObj = JSON.parse(currentContent)
|
|
47
|
+
const formattedJson = JSON.stringify(jsonObj, null, 2)
|
|
48
|
+
updateFileContent(currentFile, formattedJson)
|
|
49
|
+
} catch (jsonError) {
|
|
50
|
+
toast({
|
|
51
|
+
title: "Invalid JSON",
|
|
52
|
+
description: "Failed to format JSON: invalid syntax.",
|
|
53
|
+
variant: "destructive",
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const formattedCode = window.prettier.format(currentContent, {
|
|
61
|
+
semi: false,
|
|
62
|
+
parser: "typescript",
|
|
63
|
+
plugins: window.prettierPlugins,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
updateFileContent(currentFile, formattedCode)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Formatting error:", error)
|
|
69
|
+
toast({
|
|
70
|
+
title: "Formatting error",
|
|
71
|
+
description:
|
|
72
|
+
error instanceof Error
|
|
73
|
+
? error.message
|
|
74
|
+
: "Failed to format the code. Please check for syntax errors.",
|
|
75
|
+
variant: "destructive",
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}, [currentFile, files, toast, updateFileContent])
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<div className="flex items-center gap-2 px-2 border-b border-gray-200">
|
|
83
|
+
<button
|
|
84
|
+
className={`text-black/60 scale-90 transition-opacity duration-200 ${sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
|
85
|
+
onClick={() => setSidebarOpen(true)}
|
|
86
|
+
>
|
|
87
|
+
<PanelRightClose />
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<div className="flex items-center gap-2 px-2 py-1 ml-auto">
|
|
91
|
+
{checkIfManualEditsImported(files) && (
|
|
92
|
+
<DropdownMenu>
|
|
93
|
+
<DropdownMenuTrigger asChild>
|
|
94
|
+
<Button
|
|
95
|
+
size="sm"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
className="text-red-500 hover:bg-red-50"
|
|
98
|
+
>
|
|
99
|
+
<AlertTriangle className="mr-2 h-4 w-4" />
|
|
100
|
+
Error
|
|
101
|
+
</Button>
|
|
102
|
+
</DropdownMenuTrigger>
|
|
103
|
+
<DropdownMenuContent>
|
|
104
|
+
<DropdownMenuItem
|
|
105
|
+
className="text-red-600 cursor-pointer"
|
|
106
|
+
onClick={() =>
|
|
107
|
+
handleManualEditsImport(files, updateFileContent, toast)
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
Manual edits exist but have not been imported. (Click to fix)
|
|
111
|
+
</DropdownMenuItem>
|
|
112
|
+
</DropdownMenuContent>
|
|
113
|
+
</DropdownMenu>
|
|
114
|
+
)}
|
|
115
|
+
<DropdownMenu>
|
|
116
|
+
<DropdownMenuTrigger asChild>
|
|
117
|
+
<Button size="sm" variant="ghost">
|
|
118
|
+
Insert
|
|
119
|
+
</Button>
|
|
120
|
+
</DropdownMenuTrigger>
|
|
121
|
+
<DropdownMenuContent>
|
|
122
|
+
<DropdownMenuItem onClick={() => setFootprintDialogOpen(true)}>
|
|
123
|
+
Chip
|
|
124
|
+
</DropdownMenuItem>
|
|
125
|
+
</DropdownMenuContent>
|
|
126
|
+
</DropdownMenu>
|
|
127
|
+
<Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
|
|
128
|
+
Import
|
|
129
|
+
</Button>
|
|
130
|
+
<Button size="sm" variant="ghost" onClick={handleFormatFile}>
|
|
131
|
+
Format
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
<ImportSnippetDialog
|
|
135
|
+
onSnippetSelected={(snippet: any) => {
|
|
136
|
+
const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
|
|
137
|
+
updateFileContent(currentFile, newContent)
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
<FootprintDialog
|
|
141
|
+
currentFile={currentFile as `${string}.${string}`}
|
|
142
|
+
open={footprintDialogOpen}
|
|
143
|
+
onOpenChange={setFootprintDialogOpen}
|
|
144
|
+
updateFileContent={updateFileContent}
|
|
145
|
+
files={files}
|
|
146
|
+
cursorPosition={cursorPosition}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default CodeEditorHeader
|