@tscircuit/fake-snippets 0.0.65 → 0.0.67
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/CONTRIBUTING.md +2 -2
- package/README.md +2 -2
- package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
- package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
- package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
- package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
- package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
- package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
- package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +1 -16
- package/bun.lock +42 -31
- package/dist/bundle.js +34 -41
- package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
- package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
- package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +3 -3
- package/package.json +7 -5
- package/src/App.tsx +0 -7
- package/src/ContextProviders.tsx +2 -0
- package/src/components/DownloadButtonAndMenu.tsx +1 -4
- package/src/components/ErrorTabContent.tsx +1 -1
- package/src/components/Footer.tsx +5 -2
- package/src/components/HeaderLogin.tsx +37 -54
- package/src/components/ImageWithFallback.tsx +37 -0
- package/src/components/JLCPCBImportDialog.tsx +43 -24
- package/src/components/PackageCard.tsx +12 -3
- package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
- package/src/components/PackageSearchResults.tsx +87 -0
- package/src/components/PackagesList.tsx +3 -3
- package/src/components/PageSearchComponent.tsx +9 -9
- package/src/components/ShippingInformationForm.tsx +1 -1
- package/src/components/TrendingPackagesCarousel.tsx +43 -23
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
- package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -22
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +25 -14
- package/src/components/ViewPackagePage/components/package-header.tsx +9 -4
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +6 -1
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +4 -4
- package/src/components/ViewPackagePage/components/sidebar.tsx +2 -14
- package/src/components/ViewSnippetSidebar.tsx +1 -1
- package/src/components/package-port/CodeEditor.tsx +13 -10
- package/src/components/package-port/CodeEditorHeader.tsx +1 -1
- package/src/components/package-port/EditorNav.tsx +2 -2
- package/src/hooks/use-get-fsmap-hash-for-package.ts +19 -0
- package/src/hooks/use-global-store.ts +1 -0
- package/src/hooks/use-preview-images.ts +20 -4
- package/src/hooks/use-shiki-highlighter.ts +13 -6
- package/src/hooks/use-toast.tsx +1 -1
- package/src/lib/download-fns/download-gltf.ts +3 -10
- package/src/lib/handleManualEditsImport.tsx +1 -1
- package/src/lib/types.ts +4 -2
- package/src/pages/dashboard.tsx +3 -4
- package/src/pages/editor.tsx +20 -14
- package/src/pages/latest.tsx +25 -26
- package/src/pages/package-editor.tsx +14 -2
- package/src/pages/search.tsx +120 -19
- package/src/pages/trending.tsx +14 -59
- package/src/pages/user-profile.tsx +13 -8
- package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
- package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
- package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
- package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
- package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
- package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
- package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
- package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
- package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
- package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
- package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
- package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
- package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
- package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
- package/src/components/AiChatInterface.tsx +0 -229
- package/src/components/CodeAndPreview.tsx +0 -289
- package/src/components/CodeEditor.tsx +0 -539
- package/src/components/CodeEditorHeader.tsx +0 -135
- package/src/components/EditorNav.tsx +0 -502
- package/src/components/PreviewContent.tsx +0 -372
- package/src/components/SnippetCard.tsx +0 -159
- package/src/components/SnippetList.tsx +0 -71
- package/src/hooks/use-compiled-tsx.ts +0 -37
- package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
- package/src/hooks/use-run-tsx/index.tsx +0 -256
- package/src/hooks/use-save-snippet.ts +0 -66
- package/src/hooks/use-typecheck.ts +0 -54
- package/src/lib/utils/getSyntaxError.ts +0 -13
- package/src/pages/ai.tsx +0 -92
- package/src/pages/view-snippet.tsx +0 -166
|
@@ -1,539 +0,0 @@
|
|
|
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 "./CodeEditorHeader"
|
|
29
|
-
// import { copilotPlugin, Language } from "@valtown/codemirror-codeium"
|
|
30
|
-
import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
|
|
31
|
-
const defaultImports = `
|
|
32
|
-
import React from "@types/react/jsx-runtime"
|
|
33
|
-
import { Circuit, createUseComponent } from "@tscircuit/core"
|
|
34
|
-
import type { CommonLayoutProps } from "@tscircuit/props"
|
|
35
|
-
`
|
|
36
|
-
import { getSingletonHighlighter, Highlighter } from "shiki"
|
|
37
|
-
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
38
|
-
|
|
39
|
-
export const CodeEditor = ({
|
|
40
|
-
onCodeChange,
|
|
41
|
-
onDtsChange,
|
|
42
|
-
readOnly = false,
|
|
43
|
-
initialCode = "",
|
|
44
|
-
manualEditsFileContent,
|
|
45
|
-
isStreaming = false,
|
|
46
|
-
showImportAndFormatButtons = true,
|
|
47
|
-
onManualEditsFileContentChanged,
|
|
48
|
-
}: {
|
|
49
|
-
onCodeChange: (code: string, filename?: string) => void
|
|
50
|
-
onDtsChange?: (dts: string) => void
|
|
51
|
-
initialCode: string
|
|
52
|
-
readOnly?: boolean
|
|
53
|
-
isStreaming?: boolean
|
|
54
|
-
manualEditsFileContent: string
|
|
55
|
-
showImportAndFormatButtons?: boolean
|
|
56
|
-
onManualEditsFileContentChanged?: (newContent: string) => void
|
|
57
|
-
}) => {
|
|
58
|
-
const editorRef = useRef<HTMLDivElement>(null)
|
|
59
|
-
const viewRef = useRef<EditorView | null>(null)
|
|
60
|
-
const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
|
|
61
|
-
const apiUrl = useSnippetsBaseApiUrl()
|
|
62
|
-
const codeCompletionApi = useCodeCompletionApi()
|
|
63
|
-
|
|
64
|
-
const { highlighter, isLoading } = useShikiHighlighter()
|
|
65
|
-
|
|
66
|
-
const [cursorPosition, setCursorPosition] = useState<number | null>(null)
|
|
67
|
-
const [code, setCode] = useState(initialCode)
|
|
68
|
-
|
|
69
|
-
const files = useMemo(
|
|
70
|
-
() => ({
|
|
71
|
-
"index.tsx": code,
|
|
72
|
-
"manual-edits.json": manualEditsFileContent,
|
|
73
|
-
}),
|
|
74
|
-
[code, manualEditsFileContent],
|
|
75
|
-
)
|
|
76
|
-
const [currentFile, setCurrentFile] =
|
|
77
|
-
useState<keyof typeof files>("index.tsx")
|
|
78
|
-
|
|
79
|
-
const isInitialCodeLoaded = Boolean(initialCode)
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (initialCode !== code) {
|
|
83
|
-
setCode(initialCode)
|
|
84
|
-
if (currentFile === "index.tsx") {
|
|
85
|
-
updateCurrentEditorContent(initialCode)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}, [isInitialCodeLoaded])
|
|
89
|
-
|
|
90
|
-
// Whenever streaming completes, reset the code to the initial code
|
|
91
|
-
useEffect(() => {
|
|
92
|
-
if (!isStreaming && code !== initialCode && initialCode) {
|
|
93
|
-
setCode(initialCode)
|
|
94
|
-
|
|
95
|
-
// HACK: Timeout because we need to wait for the editor to mount again
|
|
96
|
-
setTimeout(() => {
|
|
97
|
-
updateCurrentEditorContent(initialCode)
|
|
98
|
-
}, 200)
|
|
99
|
-
}
|
|
100
|
-
}, [isStreaming])
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!editorRef.current) return
|
|
104
|
-
|
|
105
|
-
const fsMap = new Map<string, string>()
|
|
106
|
-
Object.entries(files).forEach(([filename, content]) => {
|
|
107
|
-
fsMap.set(filename, content)
|
|
108
|
-
})
|
|
109
|
-
;(window as any).__DEBUG_CODE_EDITOR_FS_MAP = fsMap
|
|
110
|
-
|
|
111
|
-
createDefaultMapFromCDN(
|
|
112
|
-
{ target: ts.ScriptTarget.ES2022 },
|
|
113
|
-
"5.6.3",
|
|
114
|
-
true,
|
|
115
|
-
ts,
|
|
116
|
-
).then((defaultFsMap) => {
|
|
117
|
-
defaultFsMap.forEach((content, filename) => {
|
|
118
|
-
fsMap.set(filename, content)
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const system = createSystem(fsMap)
|
|
123
|
-
const env = createVirtualTypeScriptEnvironment(system, [], ts, {
|
|
124
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
125
|
-
declaration: true,
|
|
126
|
-
allowJs: true,
|
|
127
|
-
target: ts.ScriptTarget.ES2022,
|
|
128
|
-
resolveJsonModule: true,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Initialize ATA
|
|
132
|
-
const ataConfig: ATABootstrapConfig = {
|
|
133
|
-
projectName: "my-project",
|
|
134
|
-
typescript: ts,
|
|
135
|
-
logger: console,
|
|
136
|
-
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
137
|
-
const registryPrefixes = [
|
|
138
|
-
"https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
|
|
139
|
-
"https://data.jsdelivr.com/v1/package/npm/@tsci/",
|
|
140
|
-
"https://cdn.jsdelivr.net/npm/@tsci/",
|
|
141
|
-
]
|
|
142
|
-
if (
|
|
143
|
-
typeof input === "string" &&
|
|
144
|
-
registryPrefixes.some((prefix) => input.startsWith(prefix))
|
|
145
|
-
) {
|
|
146
|
-
const fullPackageName = input
|
|
147
|
-
.replace(registryPrefixes[0], "")
|
|
148
|
-
.replace(registryPrefixes[1], "")
|
|
149
|
-
.replace(registryPrefixes[2], "")
|
|
150
|
-
const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
|
|
151
|
-
const pathInPackage = fullPackageName.split("/").slice(1).join("/")
|
|
152
|
-
const jsdelivrPath = `${packageName}${pathInPackage ? `/${pathInPackage}` : ""}`
|
|
153
|
-
return fetch(
|
|
154
|
-
`${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes("/resolve/")}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
return fetch(input, init)
|
|
158
|
-
},
|
|
159
|
-
delegate: {
|
|
160
|
-
started: () => {
|
|
161
|
-
const manualEditsTypeDeclaration = `
|
|
162
|
-
declare module "*.json" {
|
|
163
|
-
const value: {
|
|
164
|
-
pcb_placements?: any[],
|
|
165
|
-
schematic_placements?: any[],
|
|
166
|
-
edit_events?: any[],
|
|
167
|
-
manual_trace_hints?: any[],
|
|
168
|
-
} | undefined;
|
|
169
|
-
export default value;
|
|
170
|
-
}
|
|
171
|
-
`
|
|
172
|
-
env.createFile("manual-edits.d.ts", manualEditsTypeDeclaration)
|
|
173
|
-
},
|
|
174
|
-
receivedFile: (code: string, path: string) => {
|
|
175
|
-
fsMap.set(path, code)
|
|
176
|
-
env.createFile(path, code)
|
|
177
|
-
if (viewRef.current) {
|
|
178
|
-
viewRef.current.dispatch({
|
|
179
|
-
changes: {
|
|
180
|
-
from: 0,
|
|
181
|
-
to: viewRef.current.state.doc.length,
|
|
182
|
-
insert: viewRef.current.state.doc.toString(),
|
|
183
|
-
},
|
|
184
|
-
selection: viewRef.current.state.selection,
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const ata = setupTypeAcquisition(ataConfig)
|
|
192
|
-
ataRef.current = ata
|
|
193
|
-
|
|
194
|
-
const lastFilesEventContent: Record<string, string> = {}
|
|
195
|
-
|
|
196
|
-
// Set up base extensions
|
|
197
|
-
const baseExtensions = [
|
|
198
|
-
basicSetup,
|
|
199
|
-
currentFile.endsWith(".json")
|
|
200
|
-
? json()
|
|
201
|
-
: javascript({ typescript: true, jsx: true }),
|
|
202
|
-
keymap.of([indentWithTab]),
|
|
203
|
-
EditorState.readOnly.of(readOnly),
|
|
204
|
-
EditorView.updateListener.of((update) => {
|
|
205
|
-
if (update.docChanged) {
|
|
206
|
-
const newContent = update.state.doc.toString()
|
|
207
|
-
|
|
208
|
-
if (newContent === lastFilesEventContent[currentFile]) return
|
|
209
|
-
lastFilesEventContent[currentFile] = newContent
|
|
210
|
-
|
|
211
|
-
if (currentFile === "index.tsx") {
|
|
212
|
-
setCode(newContent)
|
|
213
|
-
onCodeChange(newContent)
|
|
214
|
-
} else if (currentFile === "manual-edits.json") {
|
|
215
|
-
onManualEditsFileContentChanged?.(newContent)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (currentFile === "index.tsx") {
|
|
219
|
-
const { outputFiles } = env.languageService.getEmitOutput(
|
|
220
|
-
currentFile,
|
|
221
|
-
true,
|
|
222
|
-
)
|
|
223
|
-
const indexDts = outputFiles.find(
|
|
224
|
-
(file) => file.name === "index.d.ts",
|
|
225
|
-
)
|
|
226
|
-
if (indexDts?.text && onDtsChange) {
|
|
227
|
-
onDtsChange(indexDts.text)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (update.selectionSet) {
|
|
232
|
-
const pos = update.state.selection.main.head
|
|
233
|
-
setCursorPosition(pos)
|
|
234
|
-
}
|
|
235
|
-
}),
|
|
236
|
-
]
|
|
237
|
-
if (codeCompletionApi?.apiKey) {
|
|
238
|
-
baseExtensions.push(
|
|
239
|
-
// copilotPlugin({
|
|
240
|
-
// apiKey: codeCompletionApi.apiKey,
|
|
241
|
-
// language: Language.TYPESCRIPT,
|
|
242
|
-
// }),
|
|
243
|
-
EditorView.theme({
|
|
244
|
-
".cm-ghostText, .cm-ghostText *": {
|
|
245
|
-
opacity: "0.6",
|
|
246
|
-
filter: "grayscale(20%)",
|
|
247
|
-
cursor: "pointer",
|
|
248
|
-
},
|
|
249
|
-
".cm-ghostText:hover": {
|
|
250
|
-
background: "#eee",
|
|
251
|
-
},
|
|
252
|
-
".cm-content": {
|
|
253
|
-
cursor: "text",
|
|
254
|
-
},
|
|
255
|
-
".cm-underline": {
|
|
256
|
-
cursor: "text",
|
|
257
|
-
},
|
|
258
|
-
".cm-underline:hover": {
|
|
259
|
-
cursor: "pointer",
|
|
260
|
-
},
|
|
261
|
-
}),
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Add TypeScript-specific extensions and handlers
|
|
266
|
-
const tsExtensions =
|
|
267
|
-
currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")
|
|
268
|
-
? [
|
|
269
|
-
tsFacet.of({ env, path: currentFile }),
|
|
270
|
-
tsSync(),
|
|
271
|
-
tsLinter(),
|
|
272
|
-
autocompletion({ override: [tsAutocomplete()] }),
|
|
273
|
-
hoverTooltip((view, pos) => {
|
|
274
|
-
const line = view.state.doc.lineAt(pos)
|
|
275
|
-
const lineStart = line.from
|
|
276
|
-
const lineEnd = line.to
|
|
277
|
-
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
278
|
-
const matches = Array.from(
|
|
279
|
-
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
for (const match of matches) {
|
|
283
|
-
if (match.index !== undefined) {
|
|
284
|
-
const start = lineStart + match.index
|
|
285
|
-
const end = start + match[0].length
|
|
286
|
-
if (pos >= start && pos <= end) {
|
|
287
|
-
return {
|
|
288
|
-
pos: start,
|
|
289
|
-
end: end,
|
|
290
|
-
above: true,
|
|
291
|
-
create() {
|
|
292
|
-
const dom = document.createElement("div")
|
|
293
|
-
dom.textContent = "Ctrl/Cmd+Click to open snippet"
|
|
294
|
-
return { dom }
|
|
295
|
-
},
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const facet = view.state.facet(tsFacet)
|
|
302
|
-
if (!facet) return null
|
|
303
|
-
|
|
304
|
-
const { env, path } = facet
|
|
305
|
-
const info = env.languageService.getQuickInfoAtPosition(path, pos)
|
|
306
|
-
if (!info) return null
|
|
307
|
-
|
|
308
|
-
const start = info.textSpan.start
|
|
309
|
-
const end = start + info.textSpan.length
|
|
310
|
-
const content = ts.displayPartsToString(info.displayParts || [])
|
|
311
|
-
|
|
312
|
-
const dom = document.createElement("div")
|
|
313
|
-
if (highlighter) {
|
|
314
|
-
dom.innerHTML = highlighter.codeToHtml(content, {
|
|
315
|
-
lang: "typescript",
|
|
316
|
-
themes: {
|
|
317
|
-
light: "github-light",
|
|
318
|
-
dark: "github-dark",
|
|
319
|
-
},
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
pos: start,
|
|
324
|
-
end,
|
|
325
|
-
above: true,
|
|
326
|
-
create: () => ({ dom }),
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return null
|
|
330
|
-
}),
|
|
331
|
-
EditorView.domEventHandlers({
|
|
332
|
-
keydown: (event, view) => {
|
|
333
|
-
if (event.ctrlKey || event.metaKey) {
|
|
334
|
-
const content = view.contentDOM
|
|
335
|
-
const underlinedElements =
|
|
336
|
-
content.querySelectorAll<HTMLElement>(".cm-underline")
|
|
337
|
-
underlinedElements.forEach((el) => {
|
|
338
|
-
el.style.cursor = "pointer"
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
return false
|
|
342
|
-
},
|
|
343
|
-
keyup: (event, view) => {
|
|
344
|
-
if (event.key === "Control" || event.key === "Meta") {
|
|
345
|
-
const content = view.contentDOM
|
|
346
|
-
const underlinedElements =
|
|
347
|
-
content.querySelectorAll<HTMLElement>(".cm-underline")
|
|
348
|
-
underlinedElements.forEach((el) => {
|
|
349
|
-
el.style.cursor = "text"
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
return false
|
|
353
|
-
},
|
|
354
|
-
click: (event, view) => {
|
|
355
|
-
if (!event.ctrlKey && !event.metaKey) return false
|
|
356
|
-
const pos = view.posAtCoords({
|
|
357
|
-
x: event.clientX,
|
|
358
|
-
y: event.clientY,
|
|
359
|
-
})
|
|
360
|
-
if (pos === null) return false
|
|
361
|
-
|
|
362
|
-
const line = view.state.doc.lineAt(pos)
|
|
363
|
-
const lineStart = line.from
|
|
364
|
-
const lineEnd = line.to
|
|
365
|
-
const lineText = view.state.sliceDoc(lineStart, lineEnd)
|
|
366
|
-
const matches = Array.from(
|
|
367
|
-
lineText.matchAll(TSCI_PACKAGE_PATTERN),
|
|
368
|
-
)
|
|
369
|
-
for (const match of matches) {
|
|
370
|
-
if (match.index !== undefined) {
|
|
371
|
-
const start = lineStart + match.index
|
|
372
|
-
const end = start + match[0].length
|
|
373
|
-
if (pos >= start && pos <= end) {
|
|
374
|
-
const importName = match[0]
|
|
375
|
-
// Handle potential dots and dashes in package names
|
|
376
|
-
const [owner, name] = importName
|
|
377
|
-
.replace("@tsci/", "")
|
|
378
|
-
.split(".")
|
|
379
|
-
window.open(`/${owner}/${name}`, "_blank")
|
|
380
|
-
return true
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return false
|
|
385
|
-
},
|
|
386
|
-
}),
|
|
387
|
-
EditorView.theme({
|
|
388
|
-
".shiki": {
|
|
389
|
-
maxWidth: "600px",
|
|
390
|
-
padding: "12px",
|
|
391
|
-
maxHeight: "400px",
|
|
392
|
-
borderRadius: "0.5rem",
|
|
393
|
-
backgroundColor: "#fff",
|
|
394
|
-
color: "#0f172a",
|
|
395
|
-
border: "1px solid #e2e8f0",
|
|
396
|
-
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
|
|
397
|
-
fontSize: "14px",
|
|
398
|
-
fontFamily: "monospace",
|
|
399
|
-
whiteSpace: "pre-wrap",
|
|
400
|
-
lineHeight: "1.6",
|
|
401
|
-
overflow: "auto",
|
|
402
|
-
zIndex: "9999",
|
|
403
|
-
},
|
|
404
|
-
}),
|
|
405
|
-
EditorView.decorations.of((view) => {
|
|
406
|
-
const decorations = []
|
|
407
|
-
for (const { from, to } of view.visibleRanges) {
|
|
408
|
-
for (let pos = from; pos < to; ) {
|
|
409
|
-
const line = view.state.doc.lineAt(pos)
|
|
410
|
-
const lineText = line.text
|
|
411
|
-
const matches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
|
|
412
|
-
for (const match of matches) {
|
|
413
|
-
if (match.index !== undefined) {
|
|
414
|
-
const start = line.from + match.index
|
|
415
|
-
const end = start + match[0].length
|
|
416
|
-
decorations.push(
|
|
417
|
-
Decoration.mark({
|
|
418
|
-
class: "cm-underline",
|
|
419
|
-
}).range(start, end),
|
|
420
|
-
)
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
pos = line.to + 1
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return Decoration.set(decorations)
|
|
427
|
-
}),
|
|
428
|
-
]
|
|
429
|
-
: []
|
|
430
|
-
|
|
431
|
-
const state = EditorState.create({
|
|
432
|
-
doc: files[currentFile],
|
|
433
|
-
extensions: [...baseExtensions, ...tsExtensions],
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
const view = new EditorView({
|
|
437
|
-
state,
|
|
438
|
-
parent: editorRef.current,
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
viewRef.current = view
|
|
442
|
-
|
|
443
|
-
// Initial ATA run for index.tsx
|
|
444
|
-
if (currentFile === "index.tsx") {
|
|
445
|
-
ata(`${defaultImports}${code}`)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return () => {
|
|
449
|
-
view.destroy()
|
|
450
|
-
}
|
|
451
|
-
}, [!isStreaming, currentFile, code !== ""])
|
|
452
|
-
|
|
453
|
-
const updateCurrentEditorContent = (newContent: string) => {
|
|
454
|
-
if (viewRef.current) {
|
|
455
|
-
const state = viewRef.current.state
|
|
456
|
-
if (state.doc.toString() !== newContent) {
|
|
457
|
-
viewRef.current.dispatch({
|
|
458
|
-
changes: { from: 0, to: state.doc.length, insert: newContent },
|
|
459
|
-
})
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const updateEditorToMatchCurrentFile = () => {
|
|
465
|
-
const currentContent = files[currentFile] || ""
|
|
466
|
-
updateCurrentEditorContent(currentContent)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const codeImports = getImportsFromCode(code)
|
|
470
|
-
|
|
471
|
-
useEffect(() => {
|
|
472
|
-
if (ataRef.current && currentFile === "index.tsx") {
|
|
473
|
-
ataRef.current(`${defaultImports}${code}`)
|
|
474
|
-
}
|
|
475
|
-
}, [codeImports])
|
|
476
|
-
|
|
477
|
-
const handleFileChange = (filename: `${string}.${string}`) => {
|
|
478
|
-
setCurrentFile(filename as keyof typeof files)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const updateFileContent = (
|
|
482
|
-
filename: keyof typeof files,
|
|
483
|
-
newContent: string,
|
|
484
|
-
) => {
|
|
485
|
-
if (filename === "index.tsx") {
|
|
486
|
-
setCode(newContent)
|
|
487
|
-
onCodeChange(newContent)
|
|
488
|
-
} else if (filename === "manual-edits.json") {
|
|
489
|
-
onManualEditsFileContentChanged?.(newContent)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (viewRef.current && currentFile === filename) {
|
|
493
|
-
viewRef.current.dispatch({
|
|
494
|
-
changes: {
|
|
495
|
-
from: 0,
|
|
496
|
-
to: viewRef.current.state.doc.length,
|
|
497
|
-
insert: newContent,
|
|
498
|
-
},
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Whenever the current file changes, updated the editor content
|
|
504
|
-
useEffect(() => {
|
|
505
|
-
updateEditorToMatchCurrentFile()
|
|
506
|
-
}, [currentFile])
|
|
507
|
-
|
|
508
|
-
// Whenever the manual edits json content changes, update the editor if
|
|
509
|
-
// it's currently viewing the manual edits file
|
|
510
|
-
useEffect(() => {
|
|
511
|
-
if (currentFile === "manual-edits.json") {
|
|
512
|
-
updateEditorToMatchCurrentFile()
|
|
513
|
-
}
|
|
514
|
-
}, [manualEditsFileContent])
|
|
515
|
-
|
|
516
|
-
if (isStreaming) {
|
|
517
|
-
return (
|
|
518
|
-
<div className="font-mono whitespace-pre-wrap text-xs">{initialCode}</div>
|
|
519
|
-
)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return (
|
|
523
|
-
<div className="flex flex-col h-full">
|
|
524
|
-
{showImportAndFormatButtons && (
|
|
525
|
-
<CodeEditorHeader
|
|
526
|
-
currentFile={currentFile}
|
|
527
|
-
files={files}
|
|
528
|
-
handleFileChange={handleFileChange}
|
|
529
|
-
updateFileContent={(...args) => {
|
|
530
|
-
return updateFileContent(
|
|
531
|
-
...(args as Parameters<typeof updateFileContent>),
|
|
532
|
-
)
|
|
533
|
-
}}
|
|
534
|
-
/>
|
|
535
|
-
)}
|
|
536
|
-
<div ref={editorRef} className="flex-1 overflow-auto" />
|
|
537
|
-
</div>
|
|
538
|
-
)
|
|
539
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { Button } from "@/components/ui/button"
|
|
2
|
-
import {
|
|
3
|
-
Select,
|
|
4
|
-
SelectContent,
|
|
5
|
-
SelectItem,
|
|
6
|
-
SelectTrigger,
|
|
7
|
-
SelectValue,
|
|
8
|
-
} from "@/components/ui/select"
|
|
9
|
-
import { useImportSnippetDialog } from "./dialogs/import-snippet-dialog"
|
|
10
|
-
import { useToast } from "@/hooks/use-toast"
|
|
11
|
-
import {
|
|
12
|
-
DropdownMenu,
|
|
13
|
-
DropdownMenuContent,
|
|
14
|
-
DropdownMenuItem,
|
|
15
|
-
DropdownMenuTrigger,
|
|
16
|
-
} from "./ui/dropdown-menu"
|
|
17
|
-
import { AlertTriangle } from "lucide-react"
|
|
18
|
-
import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
|
|
19
|
-
import { handleManualEditsImport } from "@/lib/handleManualEditsImport"
|
|
20
|
-
|
|
21
|
-
export type FileName = "index.tsx" | "manual-edits.json" | `${string}.${string}`
|
|
22
|
-
|
|
23
|
-
interface CodeEditorHeaderProps {
|
|
24
|
-
currentFile: FileName
|
|
25
|
-
files: Record<FileName, string>
|
|
26
|
-
handleFileChange: (filename: FileName) => void
|
|
27
|
-
updateFileContent: (filename: FileName, content: string) => void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const CodeEditorHeader = ({
|
|
31
|
-
currentFile,
|
|
32
|
-
files,
|
|
33
|
-
handleFileChange,
|
|
34
|
-
updateFileContent,
|
|
35
|
-
}: CodeEditorHeaderProps) => {
|
|
36
|
-
const { Dialog: ImportSnippetDialog, openDialog: openImportDialog } =
|
|
37
|
-
useImportSnippetDialog()
|
|
38
|
-
const { toast } = useToast()
|
|
39
|
-
|
|
40
|
-
const formatCurrentFile = () => {
|
|
41
|
-
if (!window.prettier || !window.prettierPlugins) return
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const currentContent = files[currentFile]
|
|
45
|
-
|
|
46
|
-
if (currentFile.endsWith(".json")) {
|
|
47
|
-
try {
|
|
48
|
-
const jsonObj = JSON.parse(currentContent)
|
|
49
|
-
const formattedJson = JSON.stringify(jsonObj, null, 2)
|
|
50
|
-
updateFileContent(currentFile, formattedJson)
|
|
51
|
-
} catch (jsonError) {
|
|
52
|
-
throw new Error("Invalid JSON content")
|
|
53
|
-
}
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const formattedCode = window.prettier.format(currentContent, {
|
|
58
|
-
semi: false,
|
|
59
|
-
parser: "typescript",
|
|
60
|
-
plugins: window.prettierPlugins,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
updateFileContent(currentFile, formattedCode)
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error("Formatting error:", error)
|
|
66
|
-
toast({
|
|
67
|
-
title: "Formatting error",
|
|
68
|
-
description:
|
|
69
|
-
error instanceof Error
|
|
70
|
-
? error.message
|
|
71
|
-
: "Failed to format the code. Please check for syntax errors.",
|
|
72
|
-
variant: "destructive",
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="flex items-center gap-2 px-2 border-b border-gray-200">
|
|
79
|
-
<div>
|
|
80
|
-
<Select value={currentFile} onValueChange={handleFileChange}>
|
|
81
|
-
<SelectTrigger className="h-7 px-3 bg-white">
|
|
82
|
-
<SelectValue placeholder="Select file" />
|
|
83
|
-
</SelectTrigger>
|
|
84
|
-
<SelectContent>
|
|
85
|
-
{Object.keys(files).map((filename) => (
|
|
86
|
-
<SelectItem className="py-1" key={filename} value={filename}>
|
|
87
|
-
<span className="text-xs pr-1">{filename}</span>
|
|
88
|
-
</SelectItem>
|
|
89
|
-
))}
|
|
90
|
-
</SelectContent>
|
|
91
|
-
</Select>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="flex items-center gap-2 px-2 py-1 ml-auto">
|
|
94
|
-
{checkIfManualEditsImported(files) && (
|
|
95
|
-
<DropdownMenu>
|
|
96
|
-
<DropdownMenuTrigger asChild>
|
|
97
|
-
<Button
|
|
98
|
-
size="sm"
|
|
99
|
-
variant="ghost"
|
|
100
|
-
className="text-red-500 hover:bg-red-50"
|
|
101
|
-
>
|
|
102
|
-
<AlertTriangle className="mr-2 h-4 w-4" />
|
|
103
|
-
Error
|
|
104
|
-
</Button>
|
|
105
|
-
</DropdownMenuTrigger>
|
|
106
|
-
<DropdownMenuContent>
|
|
107
|
-
<DropdownMenuItem
|
|
108
|
-
className="text-red-600 cursor-pointer"
|
|
109
|
-
onClick={() =>
|
|
110
|
-
handleManualEditsImport(files, updateFileContent, toast)
|
|
111
|
-
}
|
|
112
|
-
>
|
|
113
|
-
Manual edits exist but have not been imported. (Click to fix)
|
|
114
|
-
</DropdownMenuItem>
|
|
115
|
-
</DropdownMenuContent>
|
|
116
|
-
</DropdownMenu>
|
|
117
|
-
)}
|
|
118
|
-
<Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
|
|
119
|
-
Import
|
|
120
|
-
</Button>
|
|
121
|
-
<Button size="sm" variant="ghost" onClick={formatCurrentFile}>
|
|
122
|
-
Format
|
|
123
|
-
</Button>
|
|
124
|
-
</div>
|
|
125
|
-
<ImportSnippetDialog
|
|
126
|
-
onSnippetSelected={(snippet) => {
|
|
127
|
-
const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
|
|
128
|
-
updateFileContent(currentFile, newContent)
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export default CodeEditorHeader
|