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;
|
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} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { 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 (
|
||||||
|
Reference in New Issue
Block a user