Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@
"tauri": "tauri"
},
"dependencies": {
"@babel/runtime": "^7.28.2",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.3.2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-shell": "^2.3.0",
"@uiw/codemirror-themes-all": "^4.24.2",
"@vueuse/core": "^13.6.0",
"codemirror": "^6.0.2",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.539.0",
"vue": "^3.5.13",
"vue-codemirror": "^6.1.1",
"vue3-markdown-it": "^1.0.10"
},
"devDependencies": {
Expand Down
28 changes: 28 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ use tauri::{AppHandle, Manager, command};

static CONFIG_MANAGER: Mutex<Option<ConfigManager>> = Mutex::new(None);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditorConfig {
pub indent_with_tab: Option<bool>, // 是否使用 tab 缩进
pub tab_size: Option<u32>, // tab 缩进, 空格数,默认为 2
pub theme: Option<String>, // 编辑器主题
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub log_directory: Option<String>,
pub auto_clear_logs: Option<bool>,
pub keep_log_days: Option<u32>,
pub theme: Option<String>,
pub plugins: Option<Vec<PluginConfig>>,
pub editor: Option<EditorConfig>,
}

impl Default for AppConfig {
Expand All @@ -27,6 +35,11 @@ impl Default for AppConfig {
keep_log_days: Some(30),
theme: Some("system".to_string()),
plugins: Some(vec![]),
editor: Some(EditorConfig {
indent_with_tab: Some(true),
tab_size: Some(2),
theme: Some("githubLight".to_string()),
}),
}
}
}
Expand Down Expand Up @@ -75,6 +88,16 @@ impl ConfigManager {
// 合并插件配置(现有配置 + 默认配置中缺失的插件)
config.plugins = Self::merge_plugins_config(config.plugins, app_handle);

// 检查并设置 editor 默认配置
if config.editor.is_none() {
config.editor = Some(EditorConfig {
indent_with_tab: Some(true),
tab_size: Some(2),
theme: Some("githubLight".to_string()),
});
println!("读取配置 -> 添加默认 editor 配置");
}

Ok(config)
}
Err(e) => {
Expand Down Expand Up @@ -170,6 +193,11 @@ impl ConfigManager {
keep_log_days: Some(30),
theme: Some("system".to_string()),
plugins: Self::get_default_plugins_config(app_handle),
editor: Some(EditorConfig {
indent_with_tab: Some(true),
tab_size: Some(2),
theme: Some("githubLight".to_string()),
}),
}
}

Expand Down
95 changes: 23 additions & 72 deletions src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,19 @@
<template>
<div class="flex bg-white h-full relative overflow-hidden">
<!-- 行号 -->
<div ref="lineNumbersRef"
class="bg-gray-50 text-gray-400 text-sm font-mono px-3 pb-4 select-none border-r border-gray-200 overflow-hidden flex-shrink-0 z-10"
style="padding-top: 0;">
<div v-for="(num, index) in lineNumbers" :key="index" class="h-6 leading-6 text-right">
{{ num }}
</div>
</div>

<!-- 语法高亮容器 -->
<div class="flex-1 relative overflow-hidden">
<!-- 高亮显示层 -->
<pre ref="highlightRef"
class="absolute inset-0 px-4 pb-4 font-mono text-sm leading-6 bg-transparent pointer-events-none overflow-auto whitespace-pre-wrap z-0"
style="margin: 0; border: 0; padding-top: 0; word-break: break-word; white-space: pre-wrap;"
v-html="highlightedCode"></pre>

<!-- 代码输入框 -->
<textarea ref="textareaRef"
:value="modelValue"
@input="handleInput"
@keydown="handleKeyDown"
@scroll="handleScroll"
class="absolute inset-0 px-4 pb-4 font-mono text-sm leading-6 resize-none outline-none bg-transparent z-10 overflow-auto"
style="color: transparent; caret-color: #374151; margin: 0; border: 0; padding-top: 0; word-break: break-word; white-space: pre-wrap;"
placeholder="在此输入代码..."
spellcheck="false">
</textarea>
</div>
<div class="flex bg-white h-full relative">
<Codemirror v-if="isReady"
style="width: 100%; height: 100%"
:model-value="modelValue"
:extensions="extensions"
:indent-with-tab="editorConfig?.indent_with_tab"
:tab-size="editorConfig?.tab_size"
@change="handleInput"/>
</div>
</template>

<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import { highlightCode } from '../utils/highlighter'
import { onMounted } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { useCodeMirrorEditor } from '../composables/useCodeMirrorEditor'

const props = defineProps<{
modelValue: string
Expand All @@ -45,46 +24,18 @@ const emit = defineEmits<{
'update:modelValue': [value: string]
}>()

const textareaRef = ref<HTMLTextAreaElement>()
const lineNumbersRef = ref<HTMLElement>()
const highlightRef = ref<HTMLPreElement>()

const lineNumbers = computed(() => {
const lines = props.modelValue.split('\n')
return lines.map((_, index) => String(index + 1))
})

const highlightedCode = computed(() => {
return highlightCode(props.modelValue, props.language || 'python3')
})
const {
isReady,
extensions,
editorConfig,
initializeEditor
} = useCodeMirrorEditor(props, emit)

const handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement
emit('update:modelValue', target.value)
const handleInput = (value: string) => {
emit('update:modelValue', value)
}

const handleKeyDown = async (e: KeyboardEvent) => {
if (e.key === 'Tab') {
e.preventDefault()
const target = e.target as HTMLTextAreaElement
const start = target.selectionStart
const end = target.selectionEnd
const newValue = props.modelValue.substring(0, start) + ' ' + props.modelValue.substring(end)
emit('update:modelValue', newValue)

await nextTick()
target.selectionStart = target.selectionEnd = start + 4
}
}

const handleScroll = (e: Event) => {
const target = e.target as HTMLTextAreaElement
if (lineNumbersRef.value) {
lineNumbersRef.value.scrollTop = target.scrollTop
}
if (highlightRef.value) {
highlightRef.value.scrollTop = target.scrollTop
highlightRef.value.scrollLeft = target.scrollLeft
}
}
</script>
onMounted(async () => {
await initializeEditor()
})
</script>
9 changes: 8 additions & 1 deletion src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<General/>
</template>

<!-- 编辑器配置 -->
<template #editor>
<Editor v-if="activeTab === 'editor'"/>
</template>

<!-- 语言配置 -->
<template #language>
<Language v-if="activeTab === 'language'"/>
Expand All @@ -21,16 +26,18 @@

<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { BracesIcon, ShieldIcon } from 'lucide-vue-next'
import { BracesIcon, CodeIcon, ShieldIcon } from 'lucide-vue-next'
import Modal from '../ui/Modal.vue'
import Tabs from '../ui/Tabs.vue'
import General from './setting/General.vue'
import Language from './setting/Language.vue'
import Editor from './setting/Editor.vue'

const isVisible = ref(false)
const activeTab = ref('general')
const tabsData = [
{ key: 'general', label: '通用', icon: ShieldIcon },
{ key: 'editor', label: '编辑器', icon: CodeIcon },
{ key: 'language', label: '语言', icon: BracesIcon }
]

Expand Down
62 changes: 62 additions & 0 deletions src/components/setting/Editor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<div v-if="editorConfig" class="space-y-2">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
是否使用 tab 缩进
</label>
<div class="flex gap-2">
<input v-model="editorConfig.indent_with_tab"
type="checkbox"
placeholder="超时时间(秒),默认 30 秒"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm"/>
</div>
</div>

<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
缩进空格数
</label>
<div class="flex gap-2">
<input v-model="editorConfig.tab_size"
type="number"
:disabled="!editorConfig.indent_with_tab"
placeholder="缩进空格数,默认 2 秒"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm"
:class="[!editorConfig.indent_with_tab ? 'opacity-50 cursor-not-allowed' : '']"/>
</div>
</div>

<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
编辑器主题
</label>
<div class="flex gap-2">
<Select v-model="editorConfig.theme"
class="w-1/4"
placeholder="选择编辑器主题"
:options="themeOptions"/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import Select from '../../ui/Select.vue'
import { useEditorConfig } from '../../composables/useEditorConfig'

const emit = defineEmits<{
'settings-changed': [config: any]
'error': [message: string]
}>()

const {
editorConfig,
themeOptions,
loadConfig
} = useEditorConfig(emit)

onMounted(async () => {
await loadConfig()
})
</script>
Loading
Loading