-
Notifications
You must be signed in to change notification settings - Fork 39
Feature/#587 implement vscode extension #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: vnext
Are you sure you want to change the base?
Changes from all commits
8683fdb
89203a1
4c4570d
ae4e3c1
e4dbfad
3ef9a71
eee9b7e
4e89e0d
f9cda6e
d117c13
2dd8381
2dd9cc5
cf24220
4fc938c
4c6573d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "name": "Run Mongo Modeler Extension", | ||
| "type": "extensionHost", | ||
| "request": "launch", | ||
| "args": [ | ||
| "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-extension" | ||
| ], | ||
| "outFiles": ["${workspaceFolder}/packages/vscode-extension/dist/**/*.mjs"] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "mongo-modeler.appUrl": "http://localhost:5173/editor.html" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export const isVSCodeEnv = (): boolean => | ||
| new URLSearchParams(window.location.search).get('env') === 'vscode'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './use-vscode-sync.hook'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { APP_MESSAGE_TYPE } from '@lemoncode/mongo-modeler-bridge-protocol'; | ||
| import { useEffect, useRef, type MutableRefObject } from 'react'; | ||
| import { useCanvasSchemaContext } from '@/core/providers'; | ||
| import { isVSCodeEnv } from './env.helpers'; | ||
| import { sendToExtension } from './vscode-bridge.helpers'; | ||
| import { serializeSchema } from './vscode-sync.helpers'; | ||
|
|
||
| const AUTO_SAVE_DEBOUNCE_MS = 500; | ||
|
|
||
| export const useVSCodeAutoSave = ( | ||
| hasReceivedFileRef: MutableRefObject<boolean> | ||
| ): void => { | ||
| const { canvasSchema } = useCanvasSchemaContext(); | ||
|
|
||
| const lastSavedContentRef = useRef<string | null>(null); | ||
| const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (!isVSCodeEnv() || !hasReceivedFileRef.current) return; | ||
|
|
||
| const content = serializeSchema(canvasSchema); | ||
|
|
||
| if (lastSavedContentRef.current === null) { | ||
| lastSavedContentRef.current = content; | ||
| return; | ||
| } | ||
|
|
||
| if (content === lastSavedContentRef.current) return; | ||
|
|
||
| debounceTimerRef.current = setTimeout(() => { | ||
| sendToExtension({ | ||
| type: APP_MESSAGE_TYPE.SAVE, | ||
| payload: { content }, | ||
| }); | ||
| lastSavedContentRef.current = content; | ||
| debounceTimerRef.current = null; | ||
| }, AUTO_SAVE_DEBOUNCE_MS); | ||
|
|
||
| return () => { | ||
| if (debounceTimerRef.current !== null) { | ||
| clearTimeout(debounceTimerRef.current); | ||
| debounceTimerRef.current = null; | ||
| } | ||
| }; | ||
| }, [canvasSchema]); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { | ||
| useCanvasSchemaContext, | ||
| useCanvasViewSettingsContext, | ||
| } from '@/core/providers'; | ||
| import { | ||
| APP_MESSAGE_TYPE, | ||
| HOST_MESSAGE_TYPE, | ||
| type LoadFilePayload, | ||
| } from '@lemoncode/mongo-modeler-bridge-protocol'; | ||
| import { useEffect, useRef, type MutableRefObject } from 'react'; | ||
| import { isVSCodeEnv } from './env.helpers'; | ||
| import { onMessage, sendToExtension } from './vscode-bridge.helpers'; | ||
| import { deserializeSchema } from './vscode-sync.helpers'; | ||
|
|
||
| export const useVSCodeFileLoad = (): MutableRefObject<boolean> => { | ||
| const { loadSchema } = useCanvasSchemaContext(); | ||
| const { setFilename, setLoadSample } = useCanvasViewSettingsContext(); | ||
|
|
||
| const loadSchemaRef = useRef(loadSchema); | ||
| const setFilenameRef = useRef(setFilename); | ||
| const setLoadSampleRef = useRef(setLoadSample); | ||
|
|
||
| useEffect(() => { | ||
| loadSchemaRef.current = loadSchema; | ||
| setFilenameRef.current = setFilename; | ||
| setLoadSampleRef.current = setLoadSample; | ||
| }); | ||
|
|
||
| const hasReceivedFileRef = useRef(false); | ||
|
|
||
| useEffect(() => { | ||
| if (!isVSCodeEnv()) return; | ||
|
|
||
| const unsubscribe = onMessage( | ||
| HOST_MESSAGE_TYPE.LOAD_FILE, | ||
| (payload: LoadFilePayload) => { | ||
| hasReceivedFileRef.current = true; | ||
| setFilenameRef.current(payload.fileName); | ||
| setLoadSampleRef.current(false); | ||
| loadSchemaRef.current(deserializeSchema(payload.data)); | ||
| } | ||
| ); | ||
|
|
||
| sendToExtension({ type: APP_MESSAGE_TYPE.WEBVIEW_READY }); | ||
|
|
||
| return unsubscribe; | ||
| }, []); | ||
|
|
||
| return hasReceivedFileRef; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { useVSCodeAutoSave } from './use-vscode-auto-save.hook'; | ||
| import { useVSCodeFileLoad } from './use-vscode-file-load.hook'; | ||
| import { useVSCodeTheme } from './use-vscode-theme.hook'; | ||
|
|
||
| /** | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 JSDoc multilínea para algo que el código ya transmite El proyecto evita JSDoc multilínea cuando el nombre del símbolo es autoexplicativo. Sugerencia: borrar el bloque o reducirlo a una línea ( |
||
| * Wires the VS Code webview bridge. The inner hooks no-op when not running | ||
| * inside a VS Code webview, so this can be called unconditionally. | ||
| */ | ||
| export const useVSCodeSync = (): void => { | ||
| const hasReceivedFileRef = useVSCodeFileLoad(); | ||
| useVSCodeAutoSave(hasReceivedFileRef); | ||
| useVSCodeTheme(); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { | ||
| HOST_MESSAGE_TYPE, | ||
| type ThemePayload, | ||
| } from '@lemoncode/mongo-modeler-bridge-protocol'; | ||
| import { useEffect } from 'react'; | ||
| import { isVSCodeEnv } from './env.helpers'; | ||
| import { onMessage } from './vscode-bridge.helpers'; | ||
|
|
||
| const CSS_VAR_MAP: Record<keyof ThemePayload, readonly string[]> = { | ||
| // VS Code sends a reduced palette; we fan out colors to existing app tokens in a simplified way. | ||
| background: ['--bg-canvas', '--background-800', '--background-900'], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Mapeo
Sugerencia: ampliar |
||
| backgroundSecondary: [ | ||
| '--bg-toolbar', | ||
| '--bg-table', | ||
| '--bg-input', | ||
| '--background-700', | ||
| '--background-400', | ||
| ], | ||
| foreground: ['--text-color'], | ||
| }; | ||
|
|
||
| const applyTheme = (theme: ThemePayload): void => { | ||
| const root = document.documentElement; | ||
| for (const [key, cssVars] of Object.entries(CSS_VAR_MAP)) { | ||
| const value = theme[key as keyof ThemePayload]; | ||
| if (!value) continue; | ||
| for (const cssVar of cssVars) { | ||
| root.style.setProperty(cssVar, value); | ||
| } | ||
| } | ||
| if (theme.background) document.body.style.backgroundColor = theme.background; | ||
| if (theme.foreground) document.body.style.color = theme.foreground; | ||
| }; | ||
|
|
||
| export const useVSCodeTheme = (): void => { | ||
| useEffect(() => { | ||
| if (!isVSCodeEnv()) return; | ||
| return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme); | ||
| }, []); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { | ||
| AppMessage, | ||
| HostMessage, | ||
| PayloadOf, | ||
| } from '@lemoncode/mongo-modeler-bridge-protocol'; | ||
| import { isVSCodeEnv } from './env.helpers'; | ||
|
|
||
| type HandlerFor<T extends HostMessage['type']> = ( | ||
| payload: PayloadOf<HostMessage, T> | ||
| ) => void; | ||
|
|
||
| type AnyHandler = (payload: unknown) => void; | ||
|
|
||
| const handlers = new Map<string, Set<AnyHandler>>(); | ||
|
|
||
| export const sendToExtension = (msg: AppMessage): void => { | ||
| if (!isVSCodeEnv()) return; | ||
| // In VS Code webviews the parent origin is not a stable app URL. | ||
| // We post to parent and rely on origin/source checks in the bridge listeners. | ||
| window.parent.postMessage(msg, '*'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴
Sugerencia: usar un origen concreto. El webview de VS Code expone el origen del host vía |
||
| }; | ||
|
|
||
| export const onMessage = <T extends HostMessage['type']>( | ||
| type: T, | ||
| handler: HandlerFor<T> | ||
| ): (() => void) => { | ||
| if (!isVSCodeEnv()) return () => { }; | ||
|
|
||
| const existing = handlers.get(type) ?? new Set<AnyHandler>(); | ||
| existing.add(handler as AnyHandler); | ||
| handlers.set(type, existing); | ||
|
|
||
| return () => { | ||
| const set = handlers.get(type); | ||
| if (!set) return; | ||
| set.delete(handler as AnyHandler); | ||
| if (set.size === 0) handlers.delete(type); | ||
| }; | ||
| }; | ||
|
|
||
| if (typeof window !== 'undefined' && isVSCodeEnv()) { | ||
| window.addEventListener('message', (event: MessageEvent) => { | ||
| if (event.source !== window.parent) return; | ||
|
|
||
| const msg = event.data as Partial<HostMessage> | undefined; | ||
| if (!msg?.type) return; | ||
|
|
||
| const set = handlers.get(msg.type); | ||
| if (!set) return; | ||
|
|
||
| const payload = (msg as { payload?: unknown }).payload; | ||
| for (const handler of set) handler(payload); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { type DatabaseSchemaVm } from '@/core/providers'; | ||
| import { mapSchemaToLatestVersion } from '@/core/providers/canvas-schema/canvas-schema.mapper'; | ||
|
|
||
| export const deserializeSchema = (data: unknown): DatabaseSchemaVm => | ||
| mapSchemaToLatestVersion(data); | ||
|
|
||
| export const serializeSchema = (schema: DatabaseSchemaVm): string => | ||
| JSON.stringify(schema, null, 2); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,22 @@ | ||
| import { isVSCodeEnv } from '@/core/vscode/env.helpers'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Import order y comentario huérfano Dos cosas en este reorder:
Sugerencia: mover |
||
| import React from 'react'; | ||
| import { | ||
| // CanvasSettingButton, | ||
| ZoomInButton, | ||
| ZoomOutButton, | ||
| ThemeToggleButton, | ||
| AboutButton, | ||
| CanvasSettingButton, | ||
| CopyButton, | ||
| DeleteButton, | ||
| ExportButton, | ||
| ImportButton, | ||
| NewButton, | ||
| OpenButton, | ||
| PasteButton, | ||
| RedoButton, | ||
| SaveButton, | ||
| ThemeToggleButton, | ||
| UndoButton, | ||
| RedoButton, | ||
| DeleteButton, | ||
| AboutButton, | ||
| CanvasSettingButton, | ||
| CopyButton, | ||
| PasteButton, | ||
| ImportButton, | ||
| // CanvasSettingButton, | ||
| ZoomInButton, | ||
| ZoomOutButton, | ||
| } from './components'; | ||
| import classes from './toolbar.pod.module.css'; | ||
|
|
||
|
|
@@ -36,7 +37,9 @@ export const ToolbarPod: React.FC = () => { | |
| <DeleteButton /> | ||
| <CanvasSettingButton /> | ||
| <AboutButton /> | ||
| <ThemeToggleButton darkLabel="Dark Mode" lightLabel="Light Mode" /> | ||
| {!isVSCodeEnv() && ( | ||
| <ThemeToggleButton darkLabel="Dark Mode" lightLabel="Light Mode" /> | ||
| )} | ||
| </header> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Debounce timer no se limpia entre renders ni en early-returns
El efecto depende de
[canvasSchema]. Cuando el schema cambia rápido, cada render entra en esteuseEffect, asigna un timer nuevo adebounceTimerRef.currenty deja al anterior huérfano (la cleanup function solo corre cuando el efecto se va a re-ejecutar o el componente se desmonta, y para entonces ya hemos pisado la referencia). Además, las early-returns de las líneas 19, 25 y 28 tampoco limpian un timer en vuelo, así que pueden disparar un SAVE con contenido obsoleto capturado en el closure.Sugerencia: limpiar el timer previo antes de programar el nuevo y al inicio del efecto: