diff --git a/package.json b/package.json
index 28da3d0..d56a93e 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,13 @@
"author": "MarkLite",
"license": "MIT",
"dependencies": {
+ "@codemirror/commands": "^6.10.3",
+ "@codemirror/lang-markdown": "^6.5.0",
+ "@codemirror/language": "^6.12.3",
+ "@codemirror/search": "^6.7.0",
+ "@codemirror/state": "^6.6.0",
+ "@codemirror/theme-one-dark": "^6.1.3",
+ "@codemirror/view": "^6.43.0",
"dexie": "^4.0.11",
"nanoid": "^5.1.5",
"react": "^18.3.1",
@@ -54,7 +61,9 @@
"target": [
{
"target": "nsis",
- "arch": ["x64"]
+ "arch": [
+ "x64"
+ ]
}
],
"icon": "assets/icon.ico"
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 888aed9..8d7c12f 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -193,7 +193,7 @@ export default function App() {
{viewMode !== 'preview' && (
-
+
)}
{viewMode !== 'editor' && }
diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx
index 61f6210..84eba1b 100644
--- a/src/renderer/components/Editor/Editor.tsx
+++ b/src/renderer/components/Editor/Editor.tsx
@@ -1,298 +1,108 @@
-import React, { useEffect, useRef, useCallback, useState } from 'react'
+import React, { useEffect, useCallback } from 'react'
import { useTabStore } from '../../stores/tabStore'
import { useSearchStore } from '../../stores/searchStore'
-import { findMatches, buildLineStarts, getLineNumber } from '../../lib/searchEngine'
+import { useCodeMirror } from './useCodeMirror'
+import { EditorToolbar } from './EditorToolbar'
-export function Editor() {
- const textareaRef = useRef(null)
- const lineNumbersRef = useRef(null)
- const wrapperRef = useRef(null)
- const highlightRef = useRef(null)
+interface EditorProps {
+ darkMode: boolean
+}
+export function Editor({ darkMode }: EditorProps) {
const activeTabId = useTabStore(s => s.activeTabId)
const tabs = useTabStore(s => s.tabs)
const activeTab = tabs.find(t => t.id === activeTabId) ?? null
- const getActiveTab = useTabStore(s => s.getActiveTab)
const updateTabContent = useTabStore(s => s.updateTabContent)
const setModified = useTabStore(s => s.setModified)
+ const updateTabScroll = useTabStore(s => s.updateTabScroll)
- // 搜索状态
- const searchIsVisible = useSearchStore(s => s.isVisible)
- const searchText = useSearchStore(s => s.searchText)
- const searchMatches = useSearchStore(s => s.matches)
- const searchCurrentIndex = useSearchStore(s => s.currentIndex)
- const searchOptions = useSearchStore(s => s.options)
+ const {
+ containerRef,
+ viewRef,
+ setContent,
+ getContent,
+ getScrollTop,
+ setScrollTop,
+ getSelection,
+ setSelection,
+ scrollTo
+ } = useCodeMirror({
+ content: activeTab?.content ?? '',
+ onChange: useCallback((value: string) => {
+ if (!activeTabId) return
+ updateTabContent(activeTabId, value)
+ setModified(activeTabId, true)
+ }, [activeTabId, updateTabContent, setModified]),
+ darkMode,
+ onScroll: useCallback((ratio: number) => {
+ window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { ratio } }))
+ }, [])
+ })
- const [lineCount, setLineCount] = useState(1)
- const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 })
- const [fileSize, setFileSize] = useState('')
- const updateTimerRef = useRef | null>(null)
-
- // 行高
- const lineHeightRef = useRef(20.8)
+ // 切换标签时加载内容
useEffect(() => {
- const textarea = textareaRef.current
- if (!textarea) return
- const style = getComputedStyle(textarea)
- const measure = document.createElement('div')
- measure.style.cssText = `position:absolute;visibility:hidden;font-family:${style.fontFamily};font-size:${style.fontSize};line-height:${style.lineHeight};white-space:pre;width:0;`
- measure.textContent = 'X'
- document.body.appendChild(measure)
- const singleHeight = measure.offsetHeight
- measure.textContent = 'X\nX'
- const doubleHeight = measure.offsetHeight
- document.body.removeChild(measure)
- lineHeightRef.current = doubleHeight - singleHeight || 20.8
- }, [])
-
- // 字符宽度
- const charWidthRef = useRef(8)
- useEffect(() => {
- const textarea = textareaRef.current
- if (!textarea) return
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- if (!ctx) return
- const style = getComputedStyle(textarea)
- ctx.font = `${style.fontSize} ${style.fontFamily}`
- charWidthRef.current = ctx.measureText('M').width
- }, [])
-
- // 加载标签内容
- useEffect(() => {
- const tab = getActiveTab()
- const textarea = textareaRef.current
- if (!tab || !textarea) return
-
- textarea.value = tab.content
- setLineCount((tab.content.match(/\n/g) || []).length + 1)
- const bytes = new Blob([tab.content]).size
- setFileSize(bytes < 1024 ? bytes + ' B' : bytes < 1048576 ? (bytes / 1024).toFixed(1) + ' KB' : (bytes / 1048576).toFixed(1) + ' MB')
+ if (!activeTab) return
+ setContent(activeTab.content)
+ // 恢复滚动和选区
requestAnimationFrame(() => {
- textarea.scrollTop = tab.scrollTop
- textarea.scrollLeft = tab.scrollLeft
- textarea.setSelectionRange(tab.selectionStart, tab.selectionEnd)
- if (lineNumbersRef.current) lineNumbersRef.current.scrollTop = tab.scrollTop
+ setScrollTop(activeTab.scrollTop)
+ setSelection(activeTab.selectionStart, activeTab.selectionEnd)
})
- }, [activeTabId, getActiveTab])
+ }, [activeTabId])
- // 更新行号
- const updateLineNumbers = useCallback((content: string) => {
- setLineCount((content.match(/\n/g) || []).length + 1)
- }, [])
-
- // 输入事件
- const handleInput = useCallback(() => {
- const textarea = textareaRef.current
- const tab = getActiveTab()
- if (!textarea || !tab) return
- const content = textarea.value
- updateTabContent(tab.id, content)
- setModified(tab.id, true)
- updateLineNumbers(content)
- const bytes = new Blob([content]).size
- setFileSize(bytes < 1024 ? bytes + ' B' : bytes < 1048576 ? (bytes / 1024).toFixed(1) + ' KB' : (bytes / 1048576).toFixed(1) + ' MB')
- }, [getActiveTab, updateTabContent, setModified, updateLineNumbers])
-
- // 滚动同步
- const handleScroll = useCallback(() => {
- const textarea = textareaRef.current
- if (!textarea) return
- if (lineNumbersRef.current) {
- lineNumbersRef.current.scrollTop = textarea.scrollTop
- }
- // 更新搜索高亮位置
- if (highlightRef.current) {
- highlightRef.current.style.top = -textarea.scrollTop + 'px'
- }
- const maxScroll = textarea.scrollHeight - textarea.clientHeight
- const ratio = maxScroll > 0 ? textarea.scrollTop / maxScroll : 0
- window.dispatchEvent(new CustomEvent('editor-scroll', { detail: { ratio } }))
- }, [])
-
- // 搜索:执行搜索并更新 matches
- const activeContent = activeTab?.content
- useEffect(() => {
- if (!searchIsVisible || !searchText) {
- if (highlightRef.current) highlightRef.current.innerHTML = ''
- return
- }
- const textarea = textareaRef.current
- if (!textarea) return
-
- const matches = findMatches(textarea.value, searchText, searchOptions)
- useSearchStore.getState().setMatches(matches)
- renderHighlights(textarea.value, matches, searchCurrentIndex)
- }, [searchIsVisible, searchText, searchOptions, activeContent])
-
- // 搜索:currentIndex 变化时滚动到匹配位置
- useEffect(() => {
- if (searchCurrentIndex < 0 || searchMatches.length === 0) return
- const textarea = textareaRef.current
- if (!textarea) return
-
- const m = searchMatches[searchCurrentIndex]
- if (!m) return
-
- // 选中匹配文本
- textarea.focus()
- textarea.setSelectionRange(m.start, m.end)
-
- // 滚动到匹配位置
- const lineStarts = buildLineStarts(textarea.value)
- const line = getLineNumber(lineStarts, m.start)
- const lineHeight = lineHeightRef.current
- const matchTop = line * lineHeight
- const viewTop = textarea.scrollTop
- const viewBottom = viewTop + textarea.clientHeight
- if (matchTop < viewTop + 40 || matchTop > viewBottom - 40) {
- textarea.scrollTop = matchTop - textarea.clientHeight / 2
- }
-
- // 重新渲染高亮
- renderHighlights(textarea.value, searchMatches, searchCurrentIndex)
- }, [searchCurrentIndex, searchMatches])
-
- // 渲染搜索高亮 overlay
- const renderHighlights = useCallback((content: string, matches: Array<{start: number; end: number}>, currentIndex: number) => {
- const wrapper = wrapperRef.current
- const textarea = textareaRef.current
- if (!wrapper || !textarea) return
-
- // 创建或复用 overlay
- if (!highlightRef.current) {
- const overlay = document.createElement('div')
- overlay.id = 'search-highlights'
- overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;height:' + textarea.scrollHeight + 'px;pointer-events:none;overflow:visible;z-index:1;'
- wrapper.style.position = 'relative'
- wrapper.appendChild(overlay)
- highlightRef.current = overlay
- }
-
- const overlay = highlightRef.current
- overlay.style.top = -textarea.scrollTop + 'px'
- overlay.style.height = textarea.scrollHeight + 'px'
- overlay.innerHTML = ''
-
- if (matches.length === 0) return
-
- const style = getComputedStyle(textarea)
- const paddingTop = parseFloat(style.paddingTop)
- const paddingLeft = parseFloat(style.paddingLeft)
- const lineHeight = lineHeightRef.current
- const charWidth = charWidthRef.current
- const lineStarts = buildLineStarts(content)
-
- for (let i = 0; i < matches.length; i++) {
- const m = matches[i]
- const startLine = getLineNumber(lineStarts, m.start)
- const startCol = m.start - lineStarts[startLine]
- const endLine = getLineNumber(lineStarts, m.end)
- const endCol = m.end - lineStarts[endLine]
-
- if (startLine === endLine) {
- // 单行匹配
- const div = document.createElement('div')
- div.className = 'search-hl' + (i === currentIndex ? ' current' : '')
- div.style.cssText = `position:absolute;top:${startLine * lineHeight + paddingTop}px;left:${startCol * charWidth + paddingLeft}px;height:${lineHeight}px;width:${Math.max((endCol - startCol) * charWidth, 8)}px;`
- overlay.appendChild(div)
- } else {
- // 多行匹配:第一行
- const div1 = document.createElement('div')
- div1.className = 'search-hl' + (i === currentIndex ? ' current' : '')
- const firstLineWidth = textarea.clientWidth - (startCol * charWidth + paddingLeft) - 20
- div1.style.cssText = `position:absolute;top:${startLine * lineHeight + paddingTop}px;left:${startCol * charWidth + paddingLeft}px;height:${lineHeight}px;width:${firstLineWidth}px;`
- overlay.appendChild(div1)
-
- // 中间行
- for (let l = startLine + 1; l < endLine; l++) {
- const divM = document.createElement('div')
- divM.className = 'search-hl' + (i === currentIndex ? ' current' : '')
- divM.style.cssText = `position:absolute;top:${l * lineHeight + paddingTop}px;left:${paddingLeft}px;height:${lineHeight}px;width:${textarea.clientWidth - paddingLeft * 2}px;`
- overlay.appendChild(divM)
- }
-
- // 最后一行
- const div2 = document.createElement('div')
- div2.className = 'search-hl' + (i === currentIndex ? ' current' : '')
- div2.style.cssText = `position:absolute;top:${endLine * lineHeight + paddingTop}px;left:${paddingLeft}px;height:${lineHeight}px;width:${endCol * charWidth}px;`
- overlay.appendChild(div2)
- }
- }
- }, [])
-
- // Tab 键
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
- if (e.key === 'Tab') {
- e.preventDefault()
- const textarea = textareaRef.current
- if (!textarea) return
- const start = textarea.selectionStart
- const end = textarea.selectionEnd
- if (start !== end) {
- const lines = textarea.value.substring(start, end).split('\n')
- const indented = lines.map(line => ' ' + line).join('\n')
- document.execCommand('insertText', false, indented)
- } else {
- document.execCommand('insertText', false, ' ')
- }
- }
- if (e.key === 'Enter' && searchIsVisible) {
- e.preventDefault()
- const state = useSearchStore.getState()
- if (e.shiftKey) state.findPrev()
- else state.findNext()
- }
- }, [searchIsVisible])
-
- // 更新光标位置
- const updateCursorPos = useCallback(() => {
- const textarea = textareaRef.current
- if (!textarea) return
- const text = textarea.value
- const pos = textarea.selectionStart
- const textBefore = text.substring(0, pos)
- const lines = textBefore.split('\n')
- setCursorPos({ line: lines.length, col: lines[lines.length - 1].length + 1 })
- }, [])
-
- // 渲染行号
- const renderLineNumbers = () => {
- const lineHeight = lineHeightRef.current
- return Array.from({ length: lineCount }, (_, i) => (
-
- {i + 1}
-
- ))
- }
-
- // 清理 overlay
+ // 保存当前标签状态(切换前)
useEffect(() => {
return () => {
- if (highlightRef.current) {
- highlightRef.current.remove()
- highlightRef.current = null
+ if (!activeTabId) return
+ updateTabScroll(activeTabId, {
+ scrollTop: getScrollTop(),
+ selectionStart: getSelection().from,
+ selectionEnd: getSelection().to
+ })
+ }
+ }, [activeTabId])
+
+ // 搜索:Ctrl+F 聚焦搜索框,Enter 导航
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ const isCtrl = e.ctrlKey || e.metaKey
+
+ // Ctrl+B 粗体
+ if (isCtrl && e.key === 'b') {
+ e.preventDefault()
+ const view = viewRef.current
+ if (!view) return
+ const { from, to } = view.state.selection.main
+ const selected = view.state.sliceDoc(from, to) || '粗体'
+ view.dispatch({
+ changes: { from, to, insert: `**${selected}**` },
+ selection: { anchor: from + 2, head: from + 2 + selected.length }
+ })
+ }
+
+ // Ctrl+I 斜体
+ if (isCtrl && e.key === 'i') {
+ e.preventDefault()
+ const view = viewRef.current
+ if (!view) return
+ const { from, to } = view.state.selection.main
+ const selected = view.state.sliceDoc(from, to) || '斜体'
+ view.dispatch({
+ changes: { from, to, insert: `*${selected}*` },
+ selection: { anchor: from + 1, head: from + 1 + selected.length }
+ })
}
}
- }, [])
+
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [viewRef])
return (
-
-
- {renderLineNumbers()}
-
-
+
)
}
diff --git a/src/renderer/components/Editor/EditorToolbar.tsx b/src/renderer/components/Editor/EditorToolbar.tsx
new file mode 100644
index 0000000..dc1aa89
--- /dev/null
+++ b/src/renderer/components/Editor/EditorToolbar.tsx
@@ -0,0 +1,107 @@
+import React, { useCallback } from 'react'
+import type { EditorView } from '@codemirror/view'
+
+interface EditorToolbarProps {
+ viewRef: React.MutableRefObject
+}
+
+export function EditorToolbar({ viewRef }: EditorToolbarProps) {
+ const insertFormatting = useCallback((before: string, after: string, placeholder: string) => {
+ const view = viewRef.current
+ if (!view) return
+
+ const { from, to } = view.state.selection.main
+ const selected = view.state.sliceDoc(from, to)
+ const text = selected || placeholder
+ const insert = before + text + after
+
+ view.dispatch({
+ changes: { from, to, insert },
+ selection: { anchor: from + before.length, head: from + before.length + text.length }
+ })
+ view.focus()
+ }, [viewRef])
+
+ const insertLinePrefix = useCallback((prefix: string) => {
+ const view = viewRef.current
+ if (!view) return
+
+ const { from } = view.state.selection.main
+ const line = view.state.doc.lineAt(from)
+ const currentLine = view.state.sliceDoc(line.from, line.to)
+
+ // 如果已经有前缀,移除它
+ if (currentLine.startsWith(prefix)) {
+ view.dispatch({
+ changes: { from: line.from, to: line.from + prefix.length, insert: '' }
+ })
+ } else {
+ view.dispatch({
+ changes: { from: line.from, to: line.from, insert: prefix }
+ })
+ }
+ view.focus()
+ }, [viewRef])
+
+ const insertBlock = useCallback((text: string) => {
+ const view = viewRef.current
+ if (!view) return
+
+ const { from } = view.state.selection.main
+ const line = view.state.doc.lineAt(from)
+ const insertPos = line.to + 1
+
+ view.dispatch({
+ changes: { from: insertPos, to: insertPos, insert: '\n' + text + '\n' },
+ selection: { anchor: insertPos + 1, head: insertPos + 1 + text.length }
+ })
+ view.focus()
+ }, [viewRef])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/renderer/components/Editor/useCodeMirror.ts b/src/renderer/components/Editor/useCodeMirror.ts
new file mode 100644
index 0000000..55b0f2c
--- /dev/null
+++ b/src/renderer/components/Editor/useCodeMirror.ts
@@ -0,0 +1,148 @@
+import { useEffect, useRef, useCallback } from 'react'
+import { EditorState, type Extension } from '@codemirror/state'
+import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightSpecialChars, drawSelection, rectangularSelection } from '@codemirror/view'
+import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
+import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
+import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter } from '@codemirror/language'
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'
+import { oneDark } from '@codemirror/theme-one-dark'
+
+interface UseCodeMirrorOptions {
+ content: string
+ onChange: (value: string) => void
+ darkMode: boolean
+ onScroll?: (ratio: number) => void
+}
+
+export function useCodeMirror({ content, onChange, darkMode, onScroll }: UseCodeMirrorOptions) {
+ const containerRef = useRef(null)
+ const viewRef = useRef(null)
+ const isExternalUpdate = useRef(false)
+
+ // 初始化编辑器
+ useEffect(() => {
+ if (!containerRef.current) return
+
+ const extensions: Extension[] = [
+ lineNumbers(),
+ highlightActiveLine(),
+ highlightSpecialChars(),
+ drawSelection(),
+ rectangularSelection(),
+ history(),
+ indentOnInput(),
+ bracketMatching(),
+ foldGutter(),
+ highlightSelectionMatches(),
+ keymap.of([
+ ...defaultKeymap,
+ ...historyKeymap,
+ ...searchKeymap,
+ indentWithTab
+ ]),
+ markdown({ base: markdownLanguage }),
+ syntaxHighlighting(defaultHighlightStyle),
+ EditorView.lineWrapping,
+ EditorView.updateListener.of(update => {
+ if (update.docChanged && !isExternalUpdate.current) {
+ onChange(update.state.doc.toString())
+ }
+ })
+ ]
+
+ if (darkMode) extensions.push(oneDark)
+
+ const state = EditorState.create({
+ doc: content,
+ extensions
+ })
+
+ const view = new EditorView({
+ state,
+ parent: containerRef.current
+ })
+
+ viewRef.current = view
+
+ // 滚动事件监听
+ const handleScroll = () => {
+ if (onScroll) {
+ const dom = view.scrollDOM
+ const maxScroll = dom.scrollHeight - dom.clientHeight
+ const ratio = maxScroll > 0 ? dom.scrollTop / maxScroll : 0
+ onScroll(ratio)
+ }
+ }
+ view.scrollDOM.addEventListener('scroll', handleScroll)
+
+ return () => {
+ view.scrollDOM.removeEventListener('scroll', handleScroll)
+ view.destroy()
+ viewRef.current = null
+ }
+ }, [darkMode]) // darkMode 变化时重建
+
+ // 外部内容更新(切换标签时)
+ const setContent = useCallback((newContent: string) => {
+ const view = viewRef.current
+ if (!view) return
+ const current = view.state.doc.toString()
+ if (current !== newContent) {
+ isExternalUpdate.current = true
+ view.dispatch({
+ changes: { from: 0, to: current.length, insert: newContent }
+ })
+ isExternalUpdate.current = false
+ }
+ }, [])
+
+ // 获取当前内容
+ const getContent = useCallback(() => {
+ return viewRef.current?.state.doc.toString() ?? ''
+ }, [])
+
+ // 获取/设置滚动位置
+ const getScrollTop = useCallback(() => {
+ return viewRef.current?.scrollDOM.scrollTop ?? 0
+ }, [])
+
+ const setScrollTop = useCallback((top: number) => {
+ if (viewRef.current) {
+ viewRef.current.scrollDOM.scrollTop = top
+ }
+ }, [])
+
+ // 获取/设置选区
+ const getSelection = useCallback(() => {
+ const view = viewRef.current
+ if (!view) return { from: 0, to: 0 }
+ const ranges = view.state.selection.ranges
+ return { from: ranges[0].from, to: ranges[0].to }
+ }, [])
+
+ const setSelection = useCallback((from: number, to: number) => {
+ const view = viewRef.current
+ if (!view) return
+ view.dispatch({ selection: { anchor: from, head: to } })
+ view.focus()
+ }, [])
+
+ // 滚动到指定位置
+ const scrollTo = useCallback((pos: number) => {
+ const view = viewRef.current
+ if (!view) return
+ view.dispatch({ effects: EditorView.scrollIntoView(pos, { y: 'center' }) })
+ }, [])
+
+ return {
+ containerRef,
+ viewRef,
+ setContent,
+ getContent,
+ getScrollTop,
+ setScrollTop,
+ getSelection,
+ setSelection,
+ scrollTo
+ }
+}
diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css
index 9a72ac1..08d44cc 100644
--- a/src/renderer/styles/global.css
+++ b/src/renderer/styles/global.css
@@ -109,51 +109,131 @@ html, body {
border-right: 1px solid var(--border);
}
-#editor-wrapper {
+/* Editor Container */
+.editor-container {
flex: 1;
display: flex;
+ flex-direction: column;
overflow: hidden;
- position: relative;
}
-#line-numbers {
- width: 50px;
+/* Editor Toolbar */
+.editor-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding: 4px 8px;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-light);
+ flex-shrink: 0;
+ overflow-x: auto;
+}
+
+.editor-toolbar::-webkit-scrollbar { height: 0; }
+
+.toolbar-btn-sm {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 28px;
+ height: 26px;
+ padding: 0 6px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-family: var(--font-ui);
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+}
+
+.toolbar-btn-sm:hover {
+ background: var(--bg-tertiary);
+ color: var(--text);
+}
+
+.toolbar-divider-sm {
+ width: 1px;
+ height: 20px;
+ background: var(--border);
+ margin: 0 4px;
+}
+
+/* CodeMirror Wrapper */
+.codemirror-wrapper {
+ flex: 1;
+ overflow: hidden;
+}
+
+.codemirror-wrapper .cm-editor {
+ height: 100%;
+ font-family: var(--font-mono);
+ font-size: 13px;
+}
+
+.codemirror-wrapper .cm-editor .cm-scroller {
+ overflow: auto;
+}
+
+.codemirror-wrapper .cm-editor .cm-content {
+ padding: 12px 0;
+}
+
+.codemirror-wrapper .cm-editor .cm-line {
+ padding: 0 16px;
+}
+
+.codemirror-wrapper .cm-editor .cm-gutters {
background: var(--bg-secondary);
border-right: 1px solid var(--border-light);
- padding: 12px 0;
- text-align: right;
- font-family: var(--font-mono);
- font-size: 13px;
- line-height: 1.6;
color: var(--text-tertiary);
- overflow: hidden;
- user-select: none;
- flex-shrink: 0;
+ font-size: 12px;
+ min-width: 40px;
}
-#line-numbers .line-num {
- padding-right: 12px;
- height: 20.8px;
+.codemirror-wrapper .cm-editor .cm-activeLineGutter {
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-#editor {
- flex: 1;
- width: 100%;
- padding: 12px 16px;
- border: none;
+.codemirror-wrapper .cm-editor .cm-activeLine {
+ background: var(--bg-tertiary);
+}
+
+.codemirror-wrapper .cm-editor.cm-focused {
outline: none;
- resize: none;
- font-family: var(--font-mono);
- font-size: 13px;
- line-height: 1.6;
- color: var(--text);
- background: var(--bg);
- tab-size: 4;
- overflow-y: auto;
- user-select: text;
}
-#editor::placeholder { color: var(--text-tertiary); }
+/* 搜索高亮 */
+.cm-searchMatch {
+ background: rgba(255, 220, 0, 0.35);
+ border-radius: 2px;
+}
+
+.cm-searchMatch.cm-searchMatch-selected {
+ background: rgba(255, 150, 0, 0.55);
+}
+
+/* Fold gutter */
+.cm-foldGutter .cm-gutterElement {
+ cursor: pointer;
+ opacity: 0.5;
+}
+
+.cm-foldGutter .cm-gutterElement:hover {
+ opacity: 1;
+}
+
+/* Selection */
+.cm-selectionBackground {
+ background: rgba(26, 115, 232, 0.2) !important;
+}
+
+.cm-editor .cm-cursor {
+ border-left-color: var(--text);
+}
/* Resizer */
#resizer {