Files
Lunar-code/src/lib/store.ts
2026-04-04 00:40:12 +02:00

269 lines
6.3 KiB
TypeScript

import { useSyncExternalStore, useCallback } from "react";
export interface EditorTab {
path: string;
name: string;
content: string;
savedContent: string;
language: string;
}
export interface SearchResult {
filePath: string;
fileName: string;
lineNumber: number;
lineContent: string;
matchStart: number;
matchEnd: number;
}
export interface EditorSettings {
fontSize: number;
fontFamily: string;
tabSize: number;
wordWrap: boolean;
autoSave: boolean;
theme: "dark" | "light";
customCssPath: string | null;
}
interface EditorStore {
tabs: EditorTab[];
activeTabPath: string | null;
workspacePath: string | null;
sidebarVisible: boolean;
terminalVisible: boolean;
sidebarView: "files" | "search";
searchQuery: string;
searchResults: SearchResult[];
searchCaseSensitive: boolean;
searchRegex: boolean;
splitEditorPath: string | null;
recentWorkspaces: string[];
fileTreeFilter: string;
cursorLine: number;
cursorCol: number;
settings: EditorSettings;
}
const defaultSettings: EditorSettings = {
fontSize: 14,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
tabSize: 2,
wordWrap: false,
autoSave: false,
theme: "dark",
customCssPath: null,
};
function loadSettings(): EditorSettings {
try {
const stored = localStorage.getItem("editor-settings");
if (stored) return { ...defaultSettings, ...JSON.parse(stored) };
} catch { /* ignore */ }
return defaultSettings;
}
function loadRecentWorkspaces(): string[] {
try {
const stored = localStorage.getItem("recent-workspaces");
if (stored) return JSON.parse(stored);
} catch { /* ignore */ }
return [];
}
let state: EditorStore = {
tabs: [],
activeTabPath: null,
workspacePath: null,
sidebarVisible: true,
terminalVisible: false,
sidebarView: "files",
searchQuery: "",
searchResults: [],
searchCaseSensitive: false,
searchRegex: false,
splitEditorPath: null,
recentWorkspaces: loadRecentWorkspaces(),
fileTreeFilter: "",
cursorLine: 1,
cursorCol: 1,
settings: loadSettings(),
};
const listeners = new Set<() => void>();
function emit() {
listeners.forEach((l) => l());
}
function getSnapshot() {
return state;
}
function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useStore() {
return useSyncExternalStore(subscribe, getSnapshot);
}
export function useStoreSelector<T>(selector: (s: EditorStore) => T): T {
const select = useCallback(() => selector(getSnapshot()), [selector]);
return useSyncExternalStore(subscribe, select);
}
export const actions = {
openFile(path: string, name: string, content: string, language: string) {
const existing = state.tabs.find((t) => t.path === path);
if (existing) {
state = { ...state, activeTabPath: path };
} else {
state = {
...state,
tabs: [...state.tabs, { path, name, content, savedContent: content, language }],
activeTabPath: path,
};
}
emit();
},
closeTab(path: string) {
const tabs = state.tabs.filter((t) => t.path !== path);
let activeTabPath = state.activeTabPath;
if (activeTabPath === path) {
const idx = state.tabs.findIndex((t) => t.path === path);
activeTabPath = tabs[Math.min(idx, tabs.length - 1)]?.path ?? null;
}
// Also close split if it was showing this file
let splitEditorPath = state.splitEditorPath;
if (splitEditorPath === path) {
splitEditorPath = null;
}
state = { ...state, tabs, activeTabPath, splitEditorPath };
emit();
},
setActiveTab(path: string) {
state = { ...state, activeTabPath: path };
emit();
},
updateContent(path: string, content: string) {
state = {
...state,
tabs: state.tabs.map((t) => (t.path === path ? { ...t, content } : t)),
};
emit();
},
markSaved(path: string) {
state = {
...state,
tabs: state.tabs.map((t) =>
t.path === path ? { ...t, savedContent: t.content } : t
),
};
emit();
},
setWorkspace(path: string | null) {
state = { ...state, workspacePath: path };
if (path) {
const recents = [path, ...state.recentWorkspaces.filter((w) => w !== path)].slice(0, 10);
state = { ...state, recentWorkspaces: recents };
localStorage.setItem("recent-workspaces", JSON.stringify(recents));
}
emit();
},
toggleSidebar() {
state = { ...state, sidebarVisible: !state.sidebarVisible };
emit();
},
toggleTerminal() {
state = { ...state, terminalVisible: !state.terminalVisible };
emit();
},
setSidebarView(view: "files" | "search") {
state = { ...state, sidebarView: view, sidebarVisible: true };
emit();
},
setSearchQuery(query: string) {
state = { ...state, searchQuery: query };
emit();
},
setSearchResults(results: SearchResult[]) {
state = { ...state, searchResults: results };
emit();
},
setSearchCaseSensitive(value: boolean) {
state = { ...state, searchCaseSensitive: value };
emit();
},
setSearchRegex(value: boolean) {
state = { ...state, searchRegex: value };
emit();
},
setSplitEditor(path: string | null) {
state = { ...state, splitEditorPath: path };
emit();
},
setFileTreeFilter(filter: string) {
state = { ...state, fileTreeFilter: filter };
emit();
},
reorderTabs(fromIndex: number, toIndex: number) {
const tabs = [...state.tabs];
const [moved] = tabs.splice(fromIndex, 1);
tabs.splice(toIndex, 0, moved);
state = { ...state, tabs };
emit();
},
reloadFileContent(path: string, content: string) {
state = {
...state,
tabs: state.tabs.map((t) =>
t.path === path ? { ...t, content, savedContent: content } : t
),
};
emit();
},
setCursor(line: number, col: number) {
state = { ...state, cursorLine: line, cursorCol: col };
emit();
},
updateTabLanguage(path: string, language: string) {
state = {
...state,
tabs: state.tabs.map((t) => (t.path === path ? { ...t, language } : t)),
};
emit();
},
updateSettings(partial: Partial<EditorSettings>) {
const settings = { ...state.settings, ...partial };
state = { ...state, settings };
localStorage.setItem("editor-settings", JSON.stringify(settings));
emit();
},
getState() {
return state;
},
};