feat: add vim mode toggle

This commit is contained in:
2024-12-02 18:09:31 +00:00
parent da5c01ccce
commit 4c34689ceb
5 changed files with 103 additions and 42 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View File

@@ -20,6 +20,7 @@ interface CodeMirrorEditorProps {
initialValue: string; initialValue: string;
className?: string; className?: string;
onValueChanged?: (path: string, value: string) => void; onValueChanged?: (path: string, value: string) => void;
vimMode: boolean;
} }
function languageExtensionFrom(path: string) { function languageExtensionFrom(path: string) {
@@ -53,12 +54,14 @@ function CodeMirrorEditor({
initialValue, initialValue,
onValueChanged, onValueChanged,
className, className,
vimMode,
}: CodeMirrorEditorProps) { }: CodeMirrorEditorProps) {
const editorElRef = useRef<HTMLDivElement | null>(null); const editorElRef = useRef<HTMLDivElement | null>(null);
const editorStates = useRef<Map<string, EditorState>>(new Map()); const editorStates = useRef<Map<string, EditorState>>(new Map());
const editorViewRef = useRef<EditorView | null>(null); const editorViewRef = useRef<EditorView | null>(null);
const uiMode = useUiMode(); const uiMode = useUiMode();
const editorThemeCompartment = useRef(new Compartment()); const editorThemeCompartment = useRef(new Compartment());
const vimCompartment = useRef(new Compartment());
// biome-ignore lint/correctness/useExhaustiveDependencies: this only needs to be called once. // biome-ignore lint/correctness/useExhaustiveDependencies: this only needs to be called once.
useEffect(() => { useEffect(() => {
@@ -91,10 +94,10 @@ function CodeMirrorEditor({
function createEditorState(path: string, initialValue: string) { function createEditorState(path: string, initialValue: string) {
const exts: Extension[] = [ const exts: Extension[] = [
vimCompartment.current.of(vimMode ? vim() : []),
basicSetup, basicSetup,
baseEditorTheme, baseEditorTheme,
editorThemeCompartment.current.of(uiMode === "light" ? [] : oneDark), editorThemeCompartment.current.of(uiMode === "light" ? [] : oneDark),
vim(),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
editorStates.current.set(path, update.state); editorStates.current.set(path, update.state);
if (update.docChanged) { if (update.docChanged) {
@@ -130,6 +133,12 @@ function CodeMirrorEditor({
}); });
}, [uiMode]); }, [uiMode]);
useEffect(() => {
editorViewRef.current?.dispatch({
effects: vimCompartment.current.reconfigure(vimMode ? vim() : []),
});
}, [vimMode]);
return <div className={className} ref={editorElRef} />; return <div className={className} ref={editorElRef} />;
} }

View File

@@ -9,6 +9,7 @@ interface TemplateEditorState {
currentFilePath: string; currentFilePath: string;
isBuildInProgress: boolean; isBuildInProgress: boolean;
isBuildOutputVisible: boolean; isBuildOutputVisible: boolean;
isVimModeEnabled: boolean;
buildOutput: string; buildOutput: string;
buildError: ApiErrorResponse | null; buildError: ApiErrorResponse | null;
@@ -22,6 +23,8 @@ interface TemplateEditorState {
addBuildOutputChunk: (chunk: string) => void; addBuildOutputChunk: (chunk: string) => void;
toggleBuildOutput: () => void; toggleBuildOutput: () => void;
setIsVimModeEnabled: (enabled: boolean) => void;
} }
type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>; type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>;
@@ -29,10 +32,12 @@ type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>;
function createTemplateEditorStore({ function createTemplateEditorStore({
template, template,
currentFilePath, currentFilePath,
}: { template: Template; currentFilePath: string }) { isVimModeEnabled,
}: { template: Template; currentFilePath: string; isVimModeEnabled: boolean }) {
return createStore<TemplateEditorState>()((set, get) => ({ return createStore<TemplateEditorState>()((set, get) => ({
template, template,
currentFilePath, currentFilePath,
isVimModeEnabled,
isBuildInProgress: false, isBuildInProgress: false,
isBuildOutputVisible: false, isBuildOutputVisible: false,
buildOutput: "", buildOutput: "",
@@ -78,6 +83,9 @@ function createTemplateEditorStore({
...state, ...state,
isBuildOutputVisible: !state.isBuildOutputVisible, isBuildOutputVisible: !state.isBuildOutputVisible,
})), })),
setIsVimModeEnabled: (enabled: boolean) =>
set({ isVimModeEnabled: enabled }),
})); }));
} }

View File

@@ -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 (
<header className="sticky top-0 flex shrink-0 items-center justify-between gap-2 border-b bg-background p-4">
<p className="font-bold">{currentFilePath}</p>
<div className="flex space-x-6">
<VimModeToggle />
<BuildTemplateButton />
</div>
</header>
);
}
function BuildTemplateButton() {
const isBuildInProgress = useTemplateEditorStore(
(state) => state.isBuildInProgress,
);
return (
<Dialog>
<DialogTrigger asChild>
<Button>
{isBuildInProgress ? (
<Loader2 className="animate-spin" />
) : (
<Hammer />
)}{" "}
Build
</Button>
</DialogTrigger>
<BuildTemplateDialog />
</Dialog>
);
}
function VimModeToggle() {
const id = useId();
const isVimModeEnabled = useTemplateEditorStore(
(state) => state.isVimModeEnabled,
);
const setIsVimModeEnabled = useTemplateEditorStore(
(state) => state.setIsVimModeEnabled,
);
useEffect(() => {
localStorage.setItem("vimModeEnabled", `${isVimModeEnabled}`);
}, [isVimModeEnabled]);
return (
<div className="flex items-center space-x-2">
<Switch
id={id}
checked={isVimModeEnabled}
onCheckedChange={setIsVimModeEnabled}
/>
<Label htmlFor={id}>Vim mode</Label>
</div>
);
}
export { TemplateEditorTopBar };

View File

@@ -1,7 +1,6 @@
import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api"; import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api";
import { CodeMirrorEditor } from "@/components/codemirror-editor"; import { CodeMirrorEditor } from "@/components/codemirror-editor";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -15,17 +14,10 @@ import {
} from "@/components/ui/sidebar.tsx"; } from "@/components/ui/sidebar.tsx";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Link, useRouter } from "@tanstack/react-router"; import { Link, useRouter } from "@tanstack/react-router";
import { import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
ArrowLeft, import { useEffect, useId, useRef } from "react";
ChevronDown,
ChevronUp,
Hammer,
Loader2,
} from "lucide-react";
import { useEffect, useRef } from "react";
import { useStore } from "zustand"; import { useStore } from "zustand";
import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api"; import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api";
import { BuildTemplateDialog } from "./build-template-dialog";
import { templateEditorRoute } from "./routes"; import { templateEditorRoute } from "./routes";
import { import {
type TemplateEditorStore, type TemplateEditorStore,
@@ -36,6 +28,7 @@ import {
import type { Template } from "./types"; import type { Template } from "./types";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import { TemplateEditorTopBar } from "./template-editor-top-bar";
function TemplateEditor() { function TemplateEditor() {
const { templateName, _splat } = templateEditorRoute.useParams(); const { templateName, _splat } = templateEditorRoute.useParams();
@@ -99,7 +92,11 @@ function _TemplateEditor({
}: { template: Template; currentFilePath: string }) { }: { template: Template; currentFilePath: string }) {
const store = useRef<TemplateEditorStore | null>(null); const store = useRef<TemplateEditorStore | null>(null);
if (!store.current) { if (!store.current) {
store.current = createTemplateEditorStore({ template, currentFilePath }); store.current = createTemplateEditorStore({
template,
currentFilePath,
isVimModeEnabled: localStorage.getItem("vimModeEnabled") === "true",
});
} }
const setCurrentFilePath = useStore( const setCurrentFilePath = useStore(
@@ -116,7 +113,7 @@ function _TemplateEditor({
<SidebarProvider> <SidebarProvider>
<EditorSidebar /> <EditorSidebar />
<div className="flex flex-col w-full min-w-0"> <div className="flex flex-col w-full min-w-0">
<EditorTopBar /> <TemplateEditorTopBar />
<main className="w-full h-full flex flex-col"> <main className="w-full h-full flex flex-col">
<Editor /> <Editor />
<TemplateBuildOutputPanel /> <TemplateBuildOutputPanel />
@@ -141,6 +138,9 @@ function Editor() {
const { updateTemplateFile, error: updateError } = const { updateTemplateFile, error: updateError } =
useUpdateTemplateFile(templateName); useUpdateTemplateFile(templateName);
const { toast } = useToast(); const { toast } = useToast();
const isVimModeEnabled = useTemplateEditorStore(
(state) => state.isVimModeEnabled,
);
useEffect(() => { useEffect(() => {
if (updateError) { if (updateError) {
@@ -211,38 +211,11 @@ function Editor() {
path={currentPath} path={currentPath}
initialValue={fileContent} initialValue={fileContent}
onValueChanged={onValueChanged} onValueChanged={onValueChanged}
vimMode={isVimModeEnabled}
/> />
); );
} }
function EditorTopBar() {
const currentFilePath = useTemplateEditorStore(
(state) => state.currentFilePath,
);
const isBuildInProgress = useTemplateEditorStore(
(state) => state.isBuildInProgress,
);
return (
<Dialog>
<header className="sticky top-0 flex shrink-0 items-center justify-between gap-2 border-b bg-background p-4">
<p className="font-bold">{currentFilePath}</p>
<DialogTrigger asChild>
<Button>
{isBuildInProgress ? (
<Loader2 className="animate-spin" />
) : (
<Hammer />
)}{" "}
Build
</Button>
</DialogTrigger>
</header>
<BuildTemplateDialog />
</Dialog>
);
}
function EditorSidebar() { function EditorSidebar() {
const templateName = useTemplateEditorStore((state) => state.template.name); const templateName = useTemplateEditorStore((state) => state.template.name);
return ( return (