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,
} from "@/ui/main-axios";
import { useTranslation } from "react-i18next";
import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@codemirror/theme-one-dark";
import type {
Credential,
CredentialEditorProps,
@@ -908,23 +910,31 @@ export function CredentialEditor({
</div>
</div>
<FormControl>
<textarea
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"
<CodeMirror
value={
typeof field.value === "string"
? field.value
: ""
}
onChange={(e) => {
field.onChange(e.target.value);
onChange={(value) => {
field.onChange(value);
debouncedKeyDetection(
e.target.value,
value,
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>
{detectedKeyType && (
@@ -1062,13 +1072,23 @@ export function CredentialEditor({
</Button>
</div>
<FormControl>
<textarea
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"
<CodeMirror
value={field.value || ""}
onChange={(e) => {
field.onChange(e.target.value);
debouncedPublicKeyDetection(e.target.value);
onChange={(value) => {
field.onChange(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>

View File

@@ -204,6 +204,8 @@ export function FileManagerGrid({
const gridRef = useRef<HTMLDivElement>(null);
const [editingName, setEditingName] = useState("");
// Unified drag state management
const [dragState, setDragState] = useState<DragState>({
type: "none",
@@ -1104,7 +1106,7 @@ export function FileManagerGrid({
</div>
)}
{files.length === 0 ? (
{files.length === 0 && !createIntent ? (
<div className="h-full flex items-center justify-center p-8">
<div className="text-center">
<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;
setIsLoading(true);
// Clear createIntent when changing directories
setCreateIntent(null);
try {
console.log("Loading directory:", path);
@@ -617,24 +620,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
// Linus-style creation: pure intent, no side effects
function handleCreateNewFolder() {
const defaultName = generateUniqueName("NewFolder", "directory");
setCreateIntent({
const newCreateIntent = {
id: Date.now().toString(),
type: 'directory',
type: 'directory' as const,
defaultName,
currentName: defaultName
});
console.log("Create folder intent:", defaultName);
};
setCreateIntent(newCreateIntent);
}
function handleCreateNewFile() {
const defaultName = generateUniqueName("NewFile.txt", "file");
setCreateIntent({
const newCreateIntent = {
id: Date.now().toString(),
type: 'file',
type: 'file' as const,
defaultName,
currentName: defaultName
});
console.log("Create file intent:", defaultName);
};
setCreateIntent(newCreateIntent);
}
// 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)
useEffect(() => {
if (currentHost?.id) {

View File

@@ -49,7 +49,7 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
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";
interface FileItem {
@@ -802,13 +802,27 @@ export function FileViewer({
{renderHighlightedText(editedContent)}
</div>
) : (
// Directly show editable textarea
<textarea
// Use CodeMirror for all text files (unified editor experience)
<CodeMirror
value={editedContent}
onChange={(e) => handleContentChange(e.target.value)}
className="w-full h-full p-4 border-none resize-none outline-none font-mono text-sm overflow-auto bg-background text-foreground"
onChange={(value) => handleContentChange(value)}
extensions={
getLanguageExtension(file.name)
? [getLanguageExtension(file.name)!]
: []
}
theme={oneDark}
editable={isEditable}
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>

View File

@@ -35,6 +35,8 @@ import {
} from "@/ui/main-axios.ts";
import { useTranslation } from "react-i18next";
import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
import CodeMirror from "@uiw/react-codemirror";
import { oneDark } from "@codemirror/theme-one-dark";
interface SSHHost {
id: number;
@@ -980,19 +982,25 @@ export function HostManagerEditor({
<FormItem className="mb-4">
<FormLabel>{t("hosts.sshPrivateKey")}</FormLabel>
<FormControl>
<textarea
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"
<CodeMirror
value={
typeof field.value === "string"
? field.value
: ""
}
onChange={(e) =>
field.onChange(e.target.value)
}
onChange={(value) => field.onChange(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>
</FormItem>