From 3cd4cd5823ce0fc79e3ab1aacf66e0835e8a9be5 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 14 Jan 2025 18:01:29 +0700 Subject: [PATCH 1/4] add notebook initial code --- package-lock.json | 2 + package.json | 1 + src/components/editor/javascript-editor.tsx | 113 ++++++++++++++ src/components/gui/database-gui.tsx | 3 +- src/components/gui/schema-sidebar-list.tsx | 2 +- .../gui/sql-editor/use-editor-theme.tsx | 1 + src/core/extension-manager.tsx | 10 ++ src/core/extension-menu.tsx | 4 + src/core/standard-extension.tsx | 3 +- src/extensions/notebook/index.tsx | 28 ++++ .../notebook/notebook-block-code.tsx | 116 ++++++++++++++ src/extensions/notebook/notebook-block-md.tsx | 17 +++ src/extensions/notebook/notebook-editor.tsx | 113 ++++++++++++++ src/extensions/notebook/notebook-tab.tsx | 5 + src/extensions/notebook/notebook-vm.tsx | 141 ++++++++++++++++++ 15 files changed, 556 insertions(+), 3 deletions(-) create mode 100644 src/components/editor/javascript-editor.tsx create mode 100644 src/core/extension-menu.tsx create mode 100644 src/extensions/notebook/index.tsx create mode 100644 src/extensions/notebook/notebook-block-code.tsx create mode 100644 src/extensions/notebook/notebook-block-md.tsx create mode 100644 src/extensions/notebook/notebook-editor.tsx create mode 100644 src/extensions/notebook/notebook-tab.tsx create mode 100644 src/extensions/notebook/notebook-vm.tsx diff --git a/package-lock.json b/package-lock.json index 5fd853b0..0613bcac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "@libsqlstudio/studio", "version": "0.9.0", "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.5.5", "@dagrejs/dagre": "^1.1.4", @@ -843,6 +844,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", diff --git a/package.json b/package.json index 37d07a46..6737cb11 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@libsql/client": "^0.5.3" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.5.5", "@dagrejs/dagre": "^1.1.4", diff --git a/src/components/editor/javascript-editor.tsx b/src/components/editor/javascript-editor.tsx new file mode 100644 index 00000000..16f88977 --- /dev/null +++ b/src/components/editor/javascript-editor.tsx @@ -0,0 +1,113 @@ +import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { javascript } from "@codemirror/lang-javascript"; +import { forwardRef, useMemo } from "react"; +import { tags as t } from "@lezer/highlight"; +import createTheme from "@uiw/codemirror-themes"; +import { useTheme } from "@/context/theme-provider"; +import { indentationMarkers } from "@replit/codemirror-indentation-markers"; + +interface JsonEditorProps { + value: string; + readOnly?: boolean; + onChange?: (value: string) => void; +} + +function useJavascriptTheme() { + const { theme } = useTheme(); + + return useMemo(() => { + if (theme === "light") { + return createTheme({ + theme: "light", + settings: { + background: "#FFFFFF", + foreground: "#000000", + caret: "#FBAC52", + selection: "#FFD420", + selectionMatch: "#FFD420", + gutterBackground: "#fff", + gutterForeground: "#4D4D4C", + gutterBorder: "transparent", + lineHighlight: "#00000012", + fontFamily: + 'Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace', + }, + styles: [ + { + tag: [t.propertyName, t.function(t.variableName)], + color: "#e67e22", + }, + { tag: [t.keyword], color: "#0000FF" }, + { tag: [t.comment, t.blockComment], color: "#95a5a6" }, + { tag: [t.bool, t.null], color: "#696C77" }, + { tag: [t.number], color: "#FF0080" }, + { tag: [t.string], color: "#50A14F" }, + { tag: [t.separator], color: "#383A42" }, + { tag: [t.squareBracket], color: "#383A42" }, + { tag: [t.brace], color: "#A626A4" }, + ], + }); + } else { + return createTheme({ + theme: "dark", + settings: { + background: "var(--background)", + foreground: "#9cdcfe", + caret: "#c6c6c6", + selection: "#6199ff2f", + selectionMatch: "#72a1ff59", + lineHighlight: "#ffffff0f", + gutterBackground: "var(--background)", + gutterForeground: "#838383", + gutterActiveForeground: "#fff", + fontFamily: + 'Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace', + }, + styles: [ + { tag: [t.propertyName], color: "#9b59b6" }, + { tag: [t.bool, t.null], color: "#696C77" }, + { tag: [t.number], color: "#f39c12" }, + { tag: [t.string], color: "#50A14F" }, + { tag: [t.separator], color: "#383A42" }, + { tag: [t.squareBracket], color: "#383A42" }, + { tag: [t.brace], color: "#A626A4" }, + ], + }); + } + }, [theme]); +} + +const JavascriptEditor = forwardRef( + function JavascriptEditor( + { value, onChange, readOnly }: JsonEditorProps, + ref + ) { + const theme = useJavascriptTheme(); + + return ( + + ); + } +); + +export default JavascriptEditor; diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index a0bf798d..7cf66c72 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -134,6 +134,7 @@ export default function DatabaseGui() { scc.tabs.openBuiltinQuery({}); }, }, + ...extensions.getWindowTabMenu(), databaseDriver.getFlags().supportCreateUpdateTable ? { text: "New Table", @@ -143,7 +144,7 @@ export default function DatabaseGui() { } : undefined, ].filter(Boolean) as { text: string; onClick: () => void }[]; - }, [currentSchemaName, databaseDriver]); + }, [currentSchemaName, databaseDriver, extensions]); // Send to analytic when tab changes. const previousLogTabKey = useRef(""); diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index f6cb076c..3cf4b377 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -162,7 +162,7 @@ export default function SchemaList({ search }: Readonly) { ? { title: "Edit Table", onClick: () => { - scc.tabs.openBuiltinTable({ + scc.tabs.openBuiltinSchema({ schemaName: item?.schemaName ?? currentSchemaName, tableName: item?.name, }); diff --git a/src/components/gui/sql-editor/use-editor-theme.tsx b/src/components/gui/sql-editor/use-editor-theme.tsx index 340b1006..8fb2362b 100644 --- a/src/components/gui/sql-editor/use-editor-theme.tsx +++ b/src/components/gui/sql-editor/use-editor-theme.tsx @@ -36,6 +36,7 @@ export default function useCodeEditorTheme({ { tag: [t.variableName], color: "#006600" }, { tag: [t.escape], color: "#33CC33" }, { tag: [t.tagName], color: "#1C02FF" }, + { tag: t.comment, color: "#bdc3c7" }, { tag: [t.heading], color: "#0C07FF" }, { tag: [t.quote], color: "#000000" }, { tag: [t.list], color: "#B90690" }, diff --git a/src/core/extension-manager.tsx b/src/core/extension-manager.tsx index af751931..84962d1d 100644 --- a/src/core/extension-manager.tsx +++ b/src/core/extension-manager.tsx @@ -1,5 +1,6 @@ import { ReactElement } from "react"; import { IStudioExtension } from "./extension-base"; +import { ExtensionMenuItem } from "./extension-menu"; interface RegisterSidebarOption { key: string; @@ -51,6 +52,7 @@ export class StudioExtensionManager { private sidebars: RegisterSidebarOption[] = []; private beforeQueryHandlers: BeforeQueryHandler[] = []; private afterQueryHandlers: AfterQueryHandler[] = []; + private windowTabMenu: ExtensionMenuItem[] = []; constructor(private extensions: IStudioExtension[]) {} @@ -78,6 +80,14 @@ export class StudioExtensionManager { this.afterQueryHandlers.push(handler); } + registerWindowTabMenu(menu: ExtensionMenuItem) { + this.windowTabMenu.push(menu); + } + + getWindowTabMenu(): Readonly { + return this.windowTabMenu; + } + async beforeQuery(payload: BeforeQueryPipeline) { for (const handler of this.beforeQueryHandlers) { await handler(payload); diff --git a/src/core/extension-menu.tsx b/src/core/extension-menu.tsx new file mode 100644 index 00000000..907814ed --- /dev/null +++ b/src/core/extension-menu.tsx @@ -0,0 +1,4 @@ +export interface ExtensionMenuItem { + text: string; + onClick: () => void; +} diff --git a/src/core/standard-extension.tsx b/src/core/standard-extension.tsx index 171d45f6..e7d1246a 100644 --- a/src/core/standard-extension.tsx +++ b/src/core/standard-extension.tsx @@ -2,8 +2,9 @@ * This contains the standard extensions as a base for all databases. */ +import NotebookExtension from "@/extensions/notebook"; import QueryHistoryConsoleLogExtension from "@/extensions/query-console-log"; export function createStandardExtensions() { - return [new QueryHistoryConsoleLogExtension()]; + return [new QueryHistoryConsoleLogExtension(), new NotebookExtension()]; } diff --git a/src/extensions/notebook/index.tsx b/src/extensions/notebook/index.tsx new file mode 100644 index 00000000..9c58b8e6 --- /dev/null +++ b/src/extensions/notebook/index.tsx @@ -0,0 +1,28 @@ +import { StudioExtension } from "@/core/extension-base"; +import { StudioExtensionManager } from "@/core/extension-manager"; +import { createTabExtension } from "@/core/extension-tab"; +import { NotebookIcon } from "lucide-react"; +import NotebookTab from "./notebook-tab"; + +const notebookTab = createTabExtension({ + name: "notebook", + key: () => "notebook", + generate: () => ({ + title: "Notebook", + component: , + icon: NotebookIcon, + }), +}); + +export default class NotebookExtension extends StudioExtension { + extensionName = "notebook"; + + init(studio: StudioExtensionManager): void { + studio.registerWindowTabMenu({ + text: "Notebook", + onClick: () => { + notebookTab.open({}); + }, + }); + } +} diff --git a/src/extensions/notebook/notebook-block-code.tsx b/src/extensions/notebook/notebook-block-code.tsx new file mode 100644 index 00000000..971365e9 --- /dev/null +++ b/src/extensions/notebook/notebook-block-code.tsx @@ -0,0 +1,116 @@ +import { useMemo, useState } from "react"; +import { NotebookEditorBlockValue } from "./notebook-editor"; +import { NotebookVM } from "./notebook-vm"; +import JavascriptEditor from "@/components/editor/javascript-editor"; +import { Button } from "@/components/ui/button"; +import { PlayIcon, Terminal } from "lucide-react"; +import { produce } from "immer"; + +interface OutputFormat { + type: "log"; + args: unknown[]; +} + +function OutputArgItem({ value }: { value: unknown }) { + const content = useMemo(() => { + if (typeof value === "object") { + return JSON.stringify( + value, + (_, propValue) => { + if (typeof propValue === "function") { + return propValue.toString(); + } else if (typeof propValue === "bigint") { + return propValue.toString() + "n"; + } + + return propValue; + }, + 2 + ); + } + + return (value ?? "").toString(); + }, []); + + if (content.length > 500) { + return ( + [Output too long] + ); + } + + return {content}; +} + +function OutputItem({ value }: { value: OutputFormat }) { + return ( +
+
+        {value.args.map((argValue, argIndex) => (
+          
+        ))}
+      
+
+ ); +} + +export default function NotebookBlockCode({ + value, + onChange, + vm, +}: { + vm: NotebookVM; + value: NotebookEditorBlockValue; + onChange: (value: NotebookEditorBlockValue) => void; +}) { + const [output, setOutput] = useState([]); + + const onRunClick = () => { + setOutput([]); + vm.run(value.value, { + complete: () => { + console.log("Complete"); + }, + stdOut: (data: any) => { + setOutput((prev) => [...prev, data]); + }, + stdErr: () => { + console.log("Error"); + }, + }); + }; + + const onClearLogClick = () => { + setOutput([]); + }; + + return ( +
+
+ + + +
+ + { + onChange( + produce(value, (draft) => { + draft.value = e; + }) + ); + }} + /> + +
+ {output.map((outputContent, outIdx) => ( + + ))} +
+
+ ); +} diff --git a/src/extensions/notebook/notebook-block-md.tsx b/src/extensions/notebook/notebook-block-md.tsx new file mode 100644 index 00000000..b01ed1f0 --- /dev/null +++ b/src/extensions/notebook/notebook-block-md.tsx @@ -0,0 +1,17 @@ +import { compile } from "@mdx-js/mdx"; +import { useEffect, useMemo, useState } from "react"; +import { NotebookEditorBlockValue } from "./notebook-editor"; + +export default function NotebookBlockCode({ + value, + onChange, +}: { + value: NotebookEditorBlockValue; + onChange: (value: NotebookEditorBlockValue) => void; +}) { + return ( +
+