Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402)

This commit is contained in:
Haileyesus
2026-02-25 19:07:07 +03:00
committed by GitHub
parent 23801e9cc1
commit 5e3a7b69d7
149 changed files with 11627 additions and 8453 deletions

View File

@@ -0,0 +1,126 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../../../utils/api';
import type { CodeEditorFile } from '../types/types';
type UseCodeEditorDocumentParams = {
file: CodeEditorFile;
projectPath?: string;
};
const getErrorMessage = (error: unknown) => {
if (error instanceof Error) {
return error.message;
}
return String(error);
};
export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocumentParams) => {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const fileProjectName = file.projectName ?? projectPath;
const filePath = file.path;
const fileName = file.name;
const fileDiffNewString = file.diffInfo?.new_string;
const fileDiffOldString = file.diffInfo?.old_string;
useEffect(() => {
const loadFileContent = async () => {
try {
setLoading(true);
// Diff payload may already include full old/new snapshots, so avoid disk read.
if (file.diffInfo && fileDiffNewString !== undefined && fileDiffOldString !== undefined) {
setContent(fileDiffNewString);
setLoading(false);
return;
}
if (!fileProjectName) {
throw new Error('Missing project identifier');
}
const response = await api.readFile(fileProjectName, filePath);
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
}
const data = await response.json();
setContent(data.content);
} catch (error) {
const message = getErrorMessage(error);
console.error('Error loading file:', error);
setContent(`// Error loading file: ${message}\n// File: ${fileName}\n// Path: ${filePath}`);
} finally {
setLoading(false);
}
};
loadFileContent();
}, [fileDiffNewString, fileDiffOldString, fileName, filePath, fileProjectName]);
const handleSave = useCallback(async () => {
setSaving(true);
setSaveError(null);
try {
if (!fileProjectName) {
throw new Error('Missing project identifier');
}
const response = await api.saveFile(fileProjectName, filePath, content);
if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const errorData = await response.json();
throw new Error(errorData.error || `Save failed: ${response.status}`);
}
const textError = await response.text();
console.error('Non-JSON error response:', textError);
throw new Error(`Save failed: ${response.status} ${response.statusText}`);
}
await response.json();
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
} catch (error) {
const message = getErrorMessage(error);
console.error('Error saving file:', error);
setSaveError(message);
} finally {
setSaving(false);
}
}, [content, filePath, fileProjectName]);
const handleDownload = useCallback(() => {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = file.name;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
}, [content, file.name]);
return {
content,
setContent,
loading,
saving,
saveSuccess,
saveError,
handleSave,
handleDownload,
};
};

View File

@@ -0,0 +1,85 @@
import { useEffect, useState } from 'react';
import {
CODE_EDITOR_DEFAULTS,
CODE_EDITOR_SETTINGS_CHANGED_EVENT,
CODE_EDITOR_STORAGE_KEYS,
} from '../constants/settings';
const readTheme = () => {
const savedTheme = localStorage.getItem(CODE_EDITOR_STORAGE_KEYS.theme);
if (!savedTheme) {
return CODE_EDITOR_DEFAULTS.isDarkMode;
}
return savedTheme === 'dark';
};
const readBoolean = (storageKey: string, defaultValue: boolean, falseValue = 'false') => {
const value = localStorage.getItem(storageKey);
if (value === null) {
return defaultValue;
}
return value !== falseValue;
};
const readWordWrap = () => {
return localStorage.getItem(CODE_EDITOR_STORAGE_KEYS.wordWrap) === 'true';
};
const readFontSize = () => {
const stored = localStorage.getItem(CODE_EDITOR_STORAGE_KEYS.fontSize);
return Number(stored ?? CODE_EDITOR_DEFAULTS.fontSize);
};
export const useCodeEditorSettings = () => {
const [isDarkMode, setIsDarkMode] = useState(readTheme);
const [wordWrap, setWordWrap] = useState(readWordWrap);
const [minimapEnabled, setMinimapEnabled] = useState(() => (
readBoolean(CODE_EDITOR_STORAGE_KEYS.showMinimap, CODE_EDITOR_DEFAULTS.minimapEnabled)
));
const [showLineNumbers, setShowLineNumbers] = useState(() => (
readBoolean(CODE_EDITOR_STORAGE_KEYS.lineNumbers, CODE_EDITOR_DEFAULTS.showLineNumbers)
));
const [fontSize, setFontSize] = useState(readFontSize);
// Keep legacy behavior where the editor writes theme and wrap settings directly.
useEffect(() => {
localStorage.setItem(CODE_EDITOR_STORAGE_KEYS.theme, isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
useEffect(() => {
localStorage.setItem(CODE_EDITOR_STORAGE_KEYS.wordWrap, String(wordWrap));
}, [wordWrap]);
useEffect(() => {
const refreshFromStorage = () => {
setIsDarkMode(readTheme());
setWordWrap(readWordWrap());
setMinimapEnabled(readBoolean(CODE_EDITOR_STORAGE_KEYS.showMinimap, CODE_EDITOR_DEFAULTS.minimapEnabled));
setShowLineNumbers(readBoolean(CODE_EDITOR_STORAGE_KEYS.lineNumbers, CODE_EDITOR_DEFAULTS.showLineNumbers));
setFontSize(readFontSize());
};
window.addEventListener('storage', refreshFromStorage);
window.addEventListener(CODE_EDITOR_SETTINGS_CHANGED_EVENT, refreshFromStorage);
return () => {
window.removeEventListener('storage', refreshFromStorage);
window.removeEventListener(CODE_EDITOR_SETTINGS_CHANGED_EVENT, refreshFromStorage);
};
}, []);
return {
isDarkMode,
setIsDarkMode,
wordWrap,
setWordWrap,
minimapEnabled,
setMinimapEnabled,
showLineNumbers,
setShowLineNumbers,
fontSize,
setFontSize,
};
};

View File

@@ -0,0 +1,37 @@
import { useEffect } from 'react';
type UseEditorKeyboardShortcutsParams = {
onSave: () => void;
onClose: () => void;
dependency: string;
};
export const useEditorKeyboardShortcuts = ({
onSave,
onClose,
dependency,
}: UseEditorKeyboardShortcutsParams) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
onClose();
return;
}
if (!(event.ctrlKey || event.metaKey)) {
return;
}
if (event.key.toLowerCase() === 's') {
event.preventDefault();
onSave();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [dependency, onClose, onSave]);
};

View File

@@ -0,0 +1,114 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { Project } from '../../../types/app';
import type { CodeEditorDiffInfo, CodeEditorFile } from '../types/types';
type UseEditorSidebarOptions = {
selectedProject: Project | null;
isMobile: boolean;
initialWidth?: number;
};
export const useEditorSidebar = ({
selectedProject,
isMobile,
initialWidth = 600,
}: UseEditorSidebarOptions) => {
const [editingFile, setEditingFile] = useState<CodeEditorFile | null>(null);
const [editorWidth, setEditorWidth] = useState(initialWidth);
const [editorExpanded, setEditorExpanded] = useState(false);
const [isResizing, setIsResizing] = useState(false);
const [hasManualWidth, setHasManualWidth] = useState(false);
const resizeHandleRef = useRef<HTMLDivElement | null>(null);
const handleFileOpen = useCallback(
(filePath: string, diffInfo: CodeEditorDiffInfo | null = null) => {
const normalizedPath = filePath.replace(/\\/g, '/');
const fileName = normalizedPath.split('/').pop() || filePath;
setEditingFile({
name: fileName,
path: filePath,
projectName: selectedProject?.name,
diffInfo,
});
},
[selectedProject?.name],
);
const handleCloseEditor = useCallback(() => {
setEditingFile(null);
setEditorExpanded(false);
}, []);
const handleToggleEditorExpand = useCallback(() => {
setEditorExpanded((previous) => !previous);
}, []);
const handleResizeStart = useCallback(
(event: ReactMouseEvent<HTMLDivElement>) => {
if (isMobile) {
return;
}
// After first drag interaction, the editor width is user-controlled.
setHasManualWidth(true);
setIsResizing(true);
event.preventDefault();
},
[isMobile],
);
useEffect(() => {
const handleMouseMove = (event: globalThis.MouseEvent) => {
if (!isResizing) {
return;
}
const container = resizeHandleRef.current?.parentElement;
if (!container) {
return;
}
const containerRect = container.getBoundingClientRect();
const newWidth = containerRect.right - event.clientX;
const minWidth = 300;
const maxWidth = containerRect.width * 0.8;
if (newWidth >= minWidth && newWidth <= maxWidth) {
setEditorWidth(newWidth);
}
};
const handleMouseUp = () => {
setIsResizing(false);
};
if (isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isResizing]);
return {
editingFile,
editorWidth,
editorExpanded,
hasManualWidth,
resizeHandleRef,
handleFileOpen,
handleCloseEditor,
handleToggleEditorExpand,
handleResizeStart,
};
};