269 lines
6.3 KiB
TypeScript
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;
|
|
},
|
|
};
|