Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions www/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"test": "bun test"
},
"dependencies": {
"@codemirror/lang-rust": "^6.0.1",
Expand Down Expand Up @@ -52,6 +53,7 @@
"eslint-plugin-react-refresh": "^0.3.4",
"postcss": "^8.5.25",
"prettier-plugin-tailwindcss": "^0.6.11",
"react-test-renderer": "^18.3.1",
"tailwindcss": "^4.1.3",
"typescript": "^5.0.2",
"vite": "^6.4.3"
Expand Down
11 changes: 7 additions & 4 deletions www/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { usePersistedDoc } from './hooks/use-persisted-doc';
import { useValAst } from './hooks/use-val-ast';
import { useValWasm } from './hooks/use-val-wasm';
import { examples } from './lib/examples';
import { storage } from './lib/storage';

const STORAGE_KEY_CODE = 'val-editor-code';
const STORAGE_KEY_EXAMPLE = 'val-editor-example';
Expand All @@ -29,9 +30,10 @@ function App() {
);

const [currentExample, setCurrentExample] = useState(() => {
const savedExample = localStorage.getItem(STORAGE_KEY_EXAMPLE);
const savedExample = storage.getItem(STORAGE_KEY_EXAMPLE);

return savedExample && savedExample in examples
return savedExample &&
Object.prototype.hasOwnProperty.call(examples, savedExample)
? savedExample
: DEFAULT_EXAMPLE;
});
Expand All @@ -58,11 +60,11 @@ function App() {
}, []);

useEffect(() => {
localStorage.setItem(STORAGE_KEY_EXAMPLE, currentExample);
storage.setItem(STORAGE_KEY_EXAMPLE, currentExample);
}, [currentExample]);

const handleExampleChange = (value: string) => {
if (!(value in examples)) {
if (!Object.prototype.hasOwnProperty.call(examples, value)) {
return;
}

Expand Down Expand Up @@ -95,6 +97,7 @@ function App() {
<ResizablePanelGroup
key={panelDirection}
autoSaveId={`${PANEL_LAYOUT_STORAGE_KEY}:${panelDirection}`}
storage={storage}
direction={panelDirection}
className='h-full rounded border'
>
Expand Down
35 changes: 35 additions & 0 deletions www/src/contexts/editor-settings-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,41 @@ export const defaultSettings: EditorSettings = {
tabSize: 2,
};

export function deserializeSettings(value: string): EditorSettings {
const saved: unknown = JSON.parse(value);

if (saved === null || typeof saved !== 'object' || Array.isArray(saved)) {
return defaultSettings;
}

const settings = saved as Record<string, unknown>;

return {
fontSize:
typeof settings.fontSize === 'number' &&
[12, 14, 16, 18].includes(settings.fontSize)
? settings.fontSize
: defaultSettings.fontSize,
keybindings:
settings.keybindings === 'default' || settings.keybindings === 'vim'
? settings.keybindings
: defaultSettings.keybindings,
lineNumbers:
typeof settings.lineNumbers === 'boolean'
? settings.lineNumbers
: defaultSettings.lineNumbers,
lineWrapping:
typeof settings.lineWrapping === 'boolean'
? settings.lineWrapping
: defaultSettings.lineWrapping,
tabSize:
typeof settings.tabSize === 'number' &&
[2, 4, 8].includes(settings.tabSize)
? settings.tabSize
: defaultSettings.tabSize,
};
}

type EditorSettingsContextType = {
settings: EditorSettings;
updateSettings: (settings: Partial<EditorSettings>) => void;
Expand Down
9 changes: 3 additions & 6 deletions www/src/hooks/use-persisted-doc.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { storage } from '@/lib/storage';
import { useCallback, useEffect, useState } from 'react';

export function usePersistedDoc(
key: string,
fallback: string
): [string, (value: string) => void] {
const [value, setValue] = useState<string>(() => {
const stored = window.localStorage.getItem(key);

return stored && stored.length > 0 ? stored : fallback;
});
const [value, setValue] = useState(() => storage.getItem(key) ?? fallback);

useEffect(() => {
window.localStorage.setItem(key, value);
storage.setItem(key, value);
}, [key, value]);

return [value, useCallback((next: string) => setValue(next), [])];
Expand Down
15 changes: 11 additions & 4 deletions www/src/hooks/use-persisted-state.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { storage } from '@/lib/storage';
import { useCallback, useEffect, useState } from 'react';

export function usePersistedState<T extends object>(
key: string,
initialValue: T,
options?: {
serialize?: (value: T) => string;
deserialize?: (value: string) => T;
deserialize?: (value: string) => Partial<T>;
}
): [T, (action: Partial<T> | ((prevState: T) => Partial<T>)) => void] {
const [state, setFullState] = useState<T>(() => {
const savedValue = localStorage.getItem(key);
const savedValue = storage.getItem(key);

if (savedValue !== null) {
try {
return options?.deserialize
const saved: unknown = options?.deserialize
? options.deserialize(savedValue)
: JSON.parse(savedValue);

return saved !== null &&
typeof saved === 'object' &&
!Array.isArray(saved)
? { ...initialValue, ...saved }
: initialValue;
} catch (error) {
console.warn(`Error reading ${key} from localStorage:`, error);
return initialValue;
Expand All @@ -27,7 +34,7 @@ export function usePersistedState<T extends object>(

useEffect(() => {
try {
localStorage.setItem(
storage.setItem(
key,
options?.serialize ? options.serialize(state) : JSON.stringify(state)
);
Expand Down
17 changes: 17 additions & 0 deletions www/src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const storage = {
getItem(key: string): string | null {
try {
return window.localStorage.getItem(key);
} catch {
return null;
}
},

setItem(key: string, value: string): void {
try {
window.localStorage.setItem(key, value);
} catch {
return;
}
},
};
6 changes: 5 additions & 1 deletion www/src/providers/editor-settings-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import {
EditorSettings,
EditorSettingsContext,
defaultSettings,
deserializeSettings,
} from '@/contexts/editor-settings-context';
import { usePersistedState } from '@/hooks/use-persisted-state';
import { ReactNode } from 'react';

const persistenceOptions = { deserialize: deserializeSettings };

export const EditorSettingsProvider = ({
children,
}: {
children: ReactNode;
}) => {
const [settings, setSettings] = usePersistedState<EditorSettings>(
'editor-settings',
defaultSettings
defaultSettings,
persistenceOptions
);

const updateSettings = (newSettings: Partial<EditorSettings>) => {
Expand Down
Loading
Loading