FIX: Replace all text editors with unified CodeMirror interface

This commit enhances the user experience by standardizing all text editing
components to use CodeMirror, providing consistent functionality across the
entire application.

**Text Editor Unification**:
- Replaced all textarea elements with CodeMirror editors
- Unified syntax highlighting and line numbering across all text inputs
- Consistent oneDark theme implementation throughout the application

**Fixed Components**:
- FileViewer: Enhanced file editing with syntax highlighting for all file types
- CredentialEditor: Improved SSH key editing experience with code editor features
- HostManagerEditor: Better SSH private key input with proper formatting
- FileManagerGrid: Fixed new file/folder creation in empty directories

**Key Technical Improvements**:
- Fixed oneDark theme import path from @uiw/codemirror-themes to @codemirror/theme-one-dark
- Enhanced createIntent rendering logic to work properly in empty directories
- Added automatic createIntent cleanup when navigating between directories
- Configured consistent basicSetup options across all editors

**User Experience Enhancements**:
- Professional code editing interface for all text inputs
- Line numbers and syntax highlighting for better readability
- Consistent keyboard shortcuts and editing behavior
- Improved accessibility and user interaction patterns

Users now enjoy a unified, professional editing experience whether working with
code files, configuration files, or SSH credentials. The interface is consistent,
feature-rich, and optimized for developer workflows.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-24 07:52:29 +08:00
parent fc6acbb81f
commit 2006a0a089
5 changed files with 91 additions and 37 deletions

View File

@@ -28,6 +28,8 @@ import {
generateKeyPair, generateKeyPair,
} from "@/ui/main-axios"; } from "@/ui/main-axios";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@codemirror/theme-one-dark";
import type { import type {
Credential, Credential,
CredentialEditorProps, CredentialEditorProps,
@@ -908,23 +910,31 @@ export function CredentialEditor({
</div> </div>
</div> </div>
<FormControl> <FormControl>
<textarea <CodeMirror
placeholder={t(
"placeholders.pastePrivateKey",
)}
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={ value={
typeof field.value === "string" typeof field.value === "string"
? field.value ? field.value
: "" : ""
} }
onChange={(e) => { onChange={(value) => {
field.onChange(e.target.value); field.onChange(value);
debouncedKeyDetection( debouncedKeyDetection(
e.target.value, value,
form.watch("keyPassword"), form.watch("keyPassword"),
); );
}} }}
placeholder={t("placeholders.pastePrivateKey")}
theme={oneDark}
className="border border-input rounded-md"
minHeight="120px"
basicSetup={{
lineNumbers: true,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
highlightSelectionMatches: false,
searchKeymap: false,
}}
/> />
</FormControl> </FormControl>
{detectedKeyType && ( {detectedKeyType && (
@@ -1062,13 +1072,23 @@ export function CredentialEditor({
</Button> </Button>
</div> </div>
<FormControl> <FormControl>
<textarea <CodeMirror
placeholder={t("placeholders.pastePublicKey")}
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={field.value || ""} value={field.value || ""}
onChange={(e) => { onChange={(value) => {
field.onChange(e.target.value); field.onChange(value);
debouncedPublicKeyDetection(e.target.value); debouncedPublicKeyDetection(value);
}}
placeholder={t("placeholders.pastePublicKey")}
theme={oneDark}
className="border border-input rounded-md"
minHeight="120px"
basicSetup={{
lineNumbers: true,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
highlightSelectionMatches: false,
searchKeymap: false,
}} }}
/> />
</FormControl> </FormControl>

View File

@@ -204,6 +204,8 @@ export function FileManagerGrid({
const gridRef = useRef<HTMLDivElement>(null); const gridRef = useRef<HTMLDivElement>(null);
const [editingName, setEditingName] = useState(""); const [editingName, setEditingName] = useState("");
// Unified drag state management // Unified drag state management
const [dragState, setDragState] = useState<DragState>({ const [dragState, setDragState] = useState<DragState>({
type: "none", type: "none",
@@ -1104,7 +1106,7 @@ export function FileManagerGrid({
</div> </div>
)} )}
{files.length === 0 ? ( {files.length === 0 && !createIntent ? (
<div className="h-full flex items-center justify-center p-8"> <div className="h-full flex items-center justify-center p-8">
<div className="text-center"> <div className="text-center">
<Folder className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" /> <Folder className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />

View File

@@ -309,6 +309,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
currentLoadingPathRef.current = path; currentLoadingPathRef.current = path;
setIsLoading(true); setIsLoading(true);
// Clear createIntent when changing directories
setCreateIntent(null);
try { try {
console.log("Loading directory:", path); console.log("Loading directory:", path);
@@ -617,24 +620,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
// Linus-style creation: pure intent, no side effects // Linus-style creation: pure intent, no side effects
function handleCreateNewFolder() { function handleCreateNewFolder() {
const defaultName = generateUniqueName("NewFolder", "directory"); const defaultName = generateUniqueName("NewFolder", "directory");
setCreateIntent({ const newCreateIntent = {
id: Date.now().toString(), id: Date.now().toString(),
type: 'directory', type: 'directory' as const,
defaultName, defaultName,
currentName: defaultName currentName: defaultName
}); };
console.log("Create folder intent:", defaultName);
setCreateIntent(newCreateIntent);
} }
function handleCreateNewFile() { function handleCreateNewFile() {
const defaultName = generateUniqueName("NewFile.txt", "file"); const defaultName = generateUniqueName("NewFile.txt", "file");
setCreateIntent({ const newCreateIntent = {
id: Date.now().toString(), id: Date.now().toString(),
type: 'file', type: 'file' as const,
defaultName, defaultName,
currentName: defaultName currentName: defaultName
}); };
console.log("Create file intent:", defaultName); setCreateIntent(newCreateIntent);
} }
// Handle symlink resolution // Handle symlink resolution
@@ -1549,6 +1554,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
} }
// Clear createIntent when path changes
useEffect(() => {
setCreateIntent(null);
}, [currentPath]);
// Load pinned files list (when host or connection changes) // Load pinned files list (when host or connection changes)
useEffect(() => { useEffect(() => {
if (currentHost?.id) { if (currentHost?.id) {

View File

@@ -49,7 +49,7 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@uiw/codemirror-themes"; import { oneDark } from "@codemirror/theme-one-dark";
import { languages, loadLanguage } from "@uiw/codemirror-extensions-langs"; import { languages, loadLanguage } from "@uiw/codemirror-extensions-langs";
interface FileItem { interface FileItem {
@@ -802,13 +802,27 @@ export function FileViewer({
{renderHighlightedText(editedContent)} {renderHighlightedText(editedContent)}
</div> </div>
) : ( ) : (
// Directly show editable textarea // Use CodeMirror for all text files (unified editor experience)
<textarea <CodeMirror
value={editedContent} value={editedContent}
onChange={(e) => handleContentChange(e.target.value)} onChange={(value) => handleContentChange(value)}
className="w-full h-full p-4 border-none resize-none outline-none font-mono text-sm overflow-auto bg-background text-foreground" extensions={
getLanguageExtension(file.name)
? [getLanguageExtension(file.name)!]
: []
}
theme={oneDark}
editable={isEditable}
placeholder={t("fileManager.startTyping")} placeholder={t("fileManager.startTyping")}
spellCheck={false} className="h-full text-sm"
basicSetup={{
lineNumbers: true,
foldGutter: true,
dropCursor: false,
allowMultipleSelections: false,
highlightSelectionMatches: false,
searchKeymap: true,
}}
/> />
)} )}
</div> </div>

View File

@@ -35,6 +35,8 @@ import {
} from "@/ui/main-axios.ts"; } from "@/ui/main-axios.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx"; import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@codemirror/theme-one-dark";
interface SSHHost { interface SSHHost {
id: number; id: number;
@@ -980,19 +982,25 @@ export function HostManagerEditor({
<FormItem className="mb-4"> <FormItem className="mb-4">
<FormLabel>{t("hosts.sshPrivateKey")}</FormLabel> <FormLabel>{t("hosts.sshPrivateKey")}</FormLabel>
<FormControl> <FormControl>
<textarea <CodeMirror
placeholder={t(
"placeholders.pastePrivateKey",
)}
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={ value={
typeof field.value === "string" typeof field.value === "string"
? field.value ? field.value
: "" : ""
} }
onChange={(e) => onChange={(value) => field.onChange(value)}
field.onChange(e.target.value) placeholder={t("placeholders.pastePrivateKey")}
} theme={oneDark}
className="border border-input rounded-md"
minHeight="120px"
basicSetup={{
lineNumbers: true,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
highlightSelectionMatches: false,
searchKeymap: false,
}}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>