feat: add vim mode toggle
This commit is contained in:
BIN
docs/screenshots/new-template-dialog.png
Normal file
BIN
docs/screenshots/new-template-dialog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 149 KiB |
@@ -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<HTMLDivElement | null>(null);
|
||||
const editorStates = useRef<Map<string, EditorState>>(new Map());
|
||||
const editorViewRef = useRef<EditorView | null>(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 <div className={className} ref={editorElRef} />;
|
||||
}
|
||||
|
||||
|
@@ -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<typeof createTemplateEditorStore>;
|
||||
@@ -29,10 +32,12 @@ type TemplateEditorStore = ReturnType<typeof createTemplateEditorStore>;
|
||||
function createTemplateEditorStore({
|
||||
template,
|
||||
currentFilePath,
|
||||
}: { template: Template; currentFilePath: string }) {
|
||||
isVimModeEnabled,
|
||||
}: { template: Template; currentFilePath: string; isVimModeEnabled: boolean }) {
|
||||
return createStore<TemplateEditorState>()((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 }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
71
web/src/templates/template-editor-top-bar.tsx
Normal file
71
web/src/templates/template-editor-top-bar.tsx
Normal 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 };
|
@@ -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<TemplateEditorStore | null>(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({
|
||||
<SidebarProvider>
|
||||
<EditorSidebar />
|
||||
<div className="flex flex-col w-full min-w-0">
|
||||
<EditorTopBar />
|
||||
<TemplateEditorTopBar />
|
||||
<main className="w-full h-full flex flex-col">
|
||||
<Editor />
|
||||
<TemplateBuildOutputPanel />
|
||||
@@ -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 (
|
||||
<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() {
|
||||
const templateName = useTemplateEditorStore((state) => state.template.name);
|
||||
return (
|
||||
|
Reference in New Issue
Block a user