diff --git a/docs/screenshots/new-template-dialog.png b/docs/screenshots/new-template-dialog.png new file mode 100644 index 0000000..c7e3b84 Binary files /dev/null and b/docs/screenshots/new-template-dialog.png differ diff --git a/web/src/components/codemirror-editor.tsx b/web/src/components/codemirror-editor.tsx index 6c88d7b..1f90135 100644 --- a/web/src/components/codemirror-editor.tsx +++ b/web/src/components/codemirror-editor.tsx @@ -20,6 +20,7 @@ interface CodeMirrorEditorProps { initialValue: string; className?: string; onValueChanged?: (path: string, value: string) => void; + vimMode: boolean; } function languageExtensionFrom(path: string) { @@ -53,12 +54,14 @@ function CodeMirrorEditor({ initialValue, onValueChanged, className, + vimMode, }: CodeMirrorEditorProps) { const editorElRef = useRef(null); const editorStates = useRef>(new Map()); const editorViewRef = useRef(null); const uiMode = useUiMode(); const editorThemeCompartment = useRef(new Compartment()); + const vimCompartment = useRef(new Compartment()); // biome-ignore lint/correctness/useExhaustiveDependencies: this only needs to be called once. useEffect(() => { @@ -91,10 +94,10 @@ function CodeMirrorEditor({ function createEditorState(path: string, initialValue: string) { const exts: Extension[] = [ + vimCompartment.current.of(vimMode ? vim() : []), basicSetup, baseEditorTheme, editorThemeCompartment.current.of(uiMode === "light" ? [] : oneDark), - vim(), EditorView.updateListener.of((update) => { editorStates.current.set(path, update.state); if (update.docChanged) { @@ -130,6 +133,12 @@ function CodeMirrorEditor({ }); }, [uiMode]); + useEffect(() => { + editorViewRef.current?.dispatch({ + effects: vimCompartment.current.reconfigure(vimMode ? vim() : []), + }); + }, [vimMode]); + return
; } diff --git a/web/src/templates/template-editor-store.tsx b/web/src/templates/template-editor-store.tsx index 465ea7b..321841e 100644 --- a/web/src/templates/template-editor-store.tsx +++ b/web/src/templates/template-editor-store.tsx @@ -9,6 +9,7 @@ interface TemplateEditorState { currentFilePath: string; isBuildInProgress: boolean; isBuildOutputVisible: boolean; + isVimModeEnabled: boolean; buildOutput: string; buildError: ApiErrorResponse | null; @@ -22,6 +23,8 @@ interface TemplateEditorState { addBuildOutputChunk: (chunk: string) => void; toggleBuildOutput: () => void; + + setIsVimModeEnabled: (enabled: boolean) => void; } type TemplateEditorStore = ReturnType; @@ -29,10 +32,12 @@ type TemplateEditorStore = ReturnType; function createTemplateEditorStore({ template, currentFilePath, -}: { template: Template; currentFilePath: string }) { + isVimModeEnabled, +}: { template: Template; currentFilePath: string; isVimModeEnabled: boolean }) { return createStore()((set, get) => ({ template, currentFilePath, + isVimModeEnabled, isBuildInProgress: false, isBuildOutputVisible: false, buildOutput: "", @@ -78,6 +83,9 @@ function createTemplateEditorStore({ ...state, isBuildOutputVisible: !state.isBuildOutputVisible, })), + + setIsVimModeEnabled: (enabled: boolean) => + set({ isVimModeEnabled: enabled }), })); } diff --git a/web/src/templates/template-editor-top-bar.tsx b/web/src/templates/template-editor-top-bar.tsx new file mode 100644 index 0000000..02a7a5b --- /dev/null +++ b/web/src/templates/template-editor-top-bar.tsx @@ -0,0 +1,71 @@ +import { Button } from "@/components/ui/button"; +import { Loader2, Hammer } from "lucide-react"; +import { useEffect, useId } from "react"; +import { BuildTemplateDialog } from "./build-template-dialog"; +import { useTemplateEditorStore } from "./template-editor-store"; +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; + +function TemplateEditorTopBar() { + const currentFilePath = useTemplateEditorStore( + (state) => state.currentFilePath, + ); + return ( +
+

{currentFilePath}

+
+ + +
+
+ ); +} + +function BuildTemplateButton() { + const isBuildInProgress = useTemplateEditorStore( + (state) => state.isBuildInProgress, + ); + return ( + + + + + + + ); +} + +function VimModeToggle() { + const id = useId(); + const isVimModeEnabled = useTemplateEditorStore( + (state) => state.isVimModeEnabled, + ); + const setIsVimModeEnabled = useTemplateEditorStore( + (state) => state.setIsVimModeEnabled, + ); + + useEffect(() => { + localStorage.setItem("vimModeEnabled", `${isVimModeEnabled}`); + }, [isVimModeEnabled]); + + return ( +
+ + +
+ ); +} + +export { TemplateEditorTopBar }; diff --git a/web/src/templates/template-editor.tsx b/web/src/templates/template-editor.tsx index b866f69..4dd3ba0 100644 --- a/web/src/templates/template-editor.tsx +++ b/web/src/templates/template-editor.tsx @@ -1,7 +1,6 @@ import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api"; import { CodeMirrorEditor } from "@/components/codemirror-editor"; import { Button } from "@/components/ui/button.tsx"; -import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { Sidebar, SidebarContent, @@ -15,17 +14,10 @@ import { } from "@/components/ui/sidebar.tsx"; import { cn } from "@/lib/utils"; import { Link, useRouter } from "@tanstack/react-router"; -import { - ArrowLeft, - ChevronDown, - ChevronUp, - Hammer, - Loader2, -} from "lucide-react"; -import { useEffect, useRef } from "react"; +import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react"; +import { useEffect, useId, useRef } from "react"; import { useStore } from "zustand"; import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api"; -import { BuildTemplateDialog } from "./build-template-dialog"; import { templateEditorRoute } from "./routes"; import { type TemplateEditorStore, @@ -36,6 +28,7 @@ import { import type { Template } from "./types"; import { useToast } from "@/hooks/use-toast"; import { Toaster } from "@/components/ui/toaster"; +import { TemplateEditorTopBar } from "./template-editor-top-bar"; function TemplateEditor() { const { templateName, _splat } = templateEditorRoute.useParams(); @@ -99,7 +92,11 @@ function _TemplateEditor({ }: { template: Template; currentFilePath: string }) { const store = useRef(null); if (!store.current) { - store.current = createTemplateEditorStore({ template, currentFilePath }); + store.current = createTemplateEditorStore({ + template, + currentFilePath, + isVimModeEnabled: localStorage.getItem("vimModeEnabled") === "true", + }); } const setCurrentFilePath = useStore( @@ -116,7 +113,7 @@ function _TemplateEditor({
- +
@@ -141,6 +138,9 @@ function Editor() { const { updateTemplateFile, error: updateError } = useUpdateTemplateFile(templateName); const { toast } = useToast(); + const isVimModeEnabled = useTemplateEditorStore( + (state) => state.isVimModeEnabled, + ); useEffect(() => { if (updateError) { @@ -211,38 +211,11 @@ function Editor() { path={currentPath} initialValue={fileContent} onValueChanged={onValueChanged} + vimMode={isVimModeEnabled} /> ); } -function EditorTopBar() { - const currentFilePath = useTemplateEditorStore( - (state) => state.currentFilePath, - ); - const isBuildInProgress = useTemplateEditorStore( - (state) => state.isBuildInProgress, - ); - - return ( - -
-

{currentFilePath}

- - - -
- -
- ); -} - function EditorSidebar() { const templateName = useTemplateEditorStore((state) => state.template.name); return (