@@ -535,6 +535,11 @@ import {useWorkspace} from './composables/useWorkspace'
535535import {useTextCommands } from ' ./composables/useTextCommands'
536536import {useGitPermalink } from ' ./composables/useGitPermalink'
537537import {useRevealInTree } from ' ./composables/useRevealInTree'
538+ import {useWorkspaceRoots } from ' ./composables/useWorkspaceRoots'
539+ import {useGitStatus } from ' ./composables/useGitStatus'
540+ import {useSessionTabs } from ' ./composables/useSessionTabs'
541+ import {useEditorContextMenu } from ' ./composables/useEditorContextMenu'
542+ import {useRunConfig } from ' ./composables/useRunConfig'
538543import EditorTabs from ' ./components/EditorTabs.vue'
539544import IndentControl from ' ./components/IndentControl.vue'
540545import Sidebar from ' ./components/Sidebar.vue'
@@ -732,6 +737,9 @@ const rootDir = ref<string | null>(null)
732737
733738// 远程仓库永久链接(复制 / 在浏览器打开)
734739const {copyPermalink, openPermalink} = useGitPermalink (rootDir , currentFilePath , cursorInfo )
740+
741+ // 多根工作区:额外挂载的文件夹(Git/搜索仍走主根 rootDir)
742+ const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots} = useWorkspaceRoots (rootDir )
735743const sidebarVisible = ref (kvGet (' sidebar-visible' ) === ' true' )
736744// 专注模式:隐藏顶部工具栏/运行输入/侧栏/状态栏,沉浸编辑
737745const zenMode = ref (false )
@@ -767,72 +775,11 @@ const openFolderPath = (path: string) => {
767775 sidebarVisible .value = true
768776 rememberFolder (path )
769777 // 打开新文件夹视为新工作区,清空额外挂载的根
770- extraRoots .value = []
771- kvSetJSON (WORKSPACE_EXTRA_KEY , extraRoots .value )
772- }
773-
774- // ===== 多根工作区(E3,phase 1):额外挂载的文件夹(Git/搜索仍走主根 rootDir)=====
775- const WORKSPACE_EXTRA_KEY = ' workspace-extra-roots'
776- const extraRoots = ref <string []>(kvGetJSON <string []>(WORKSPACE_EXTRA_KEY , []))
777- const addWorkspaceFolder = async () => {
778- const selected = await openDialog ({directory: true , multiple: false })
779- if (selected && typeof selected === ' string' && selected !== rootDir .value && ! extraRoots .value .includes (selected )) {
780- extraRoots .value = [... extraRoots .value , selected ]
781- kvSetJSON (WORKSPACE_EXTRA_KEY , extraRoots .value )
782- }
783- }
784- const removeWorkspaceFolder = (path : string ) => {
785- extraRoots .value = extraRoots .value .filter (p => p !== path )
786- kvSetJSON (WORKSPACE_EXTRA_KEY , extraRoots .value )
778+ resetExtraRoots ()
787779}
788780
789781// ===== 标签会话持久化 =====
790- const SESSION_TABS_KEY = ' session-tabs'
791-
792- const persistSession = () => {
793- const paths = editorTabs .value .map (t => t .filePath ).filter ((p ): p is string => !! p )
794- const pinned = editorTabs .value .filter (t => t .pinned && t .filePath ).map (t => t .filePath as string )
795- const activePath = editorTabs .value .find (t => t .id === activeTabId .value )?.filePath || null
796- kvSetJSON (SESSION_TABS_KEY , {paths , pinned , activePath })
797- }
798-
799- // 标签集合/文件/激活项/置顶变化时持久化(不含正文编辑,避免频繁写入)
800- watch (
801- () => editorTabs .value .map (t => (t .pinned ? ' *' : ' ' ) + (t .filePath || ' ' )).join (' |' ) + ' #' + activeTabId .value ,
802- () => persistSession ()
803- )
804-
805- // 启动时恢复上次打开的文件标签(仅已保存且可读的文本文件)
806- const restoreSession = async () => {
807- const saved = kvGetJSON <{ paths: string [], pinned? : string [], activePath: string | null } | null >(SESSION_TABS_KEY , null )
808- if (! saved || ! saved .paths ?.length ) {
809- return
810- }
811-
812- const limitBytes = (editorConfig .value ?.max_open_file_size ?? 5 ) * 1024 * 1024
813- for (const p of saved .paths ) {
814- try {
815- const meta = await invoke <{ size_bytes: number , is_text: boolean }>(' get_text_file_meta' , {path: p })
816- if (meta .is_text && meta .size_bytes <= limitBytes ) {
817- await openPath (p )
818- }
819- }
820- catch {
821- // 跳过已删除/无法读取的文件
822- }
823- }
824-
825- if (saved .pinned ?.length ) {
826- restorePinned (saved .pinned )
827- }
828-
829- if (saved .activePath ) {
830- const t = editorTabs .value .find (tab => tab .filePath === saved .activePath )
831- if (t ) {
832- switchTab (t .id )
833- }
834- }
835- }
782+ // 标签会话持久化见下方 useSessionTabs(待 editorConfig 声明后初始化)
836783
837784watch (sidebarVisible , (v ) => kvSet (' sidebar-visible' , String (v )))
838785
@@ -1467,15 +1414,12 @@ const onLspOpenLocation = async (e: Event) => {
14671414const showDiagnostics = ref (false )
14681415
14691416// ===== 编辑器 LSP 右键菜单(跳转定义 / 重命名 / 格式化)=====
1470- const editorCtx = reactive ({visible: false , x: 0 , y: 0 , lsp: false })
1471- const editorMenuRef = ref <HTMLElement | null >(null )
1472- const closeEditorCtx = () => {
1473- editorCtx .visible = false
1474- }
1475-
14761417// Git Blame:当前文件在已打开文件夹内时可用
14771418const blameInfo = ref <{ root: string ; rel: string ; name: string } | null >(null )
14781419const canBlame = computed (() => !! rootDir .value && !! currentFilePath .value && currentFilePath .value .startsWith (rootDir .value ))
1420+
1421+ // 编辑器右键菜单(状态/定位/全局监听抽离到 useEditorContextMenu)
1422+ const {editorCtx, editorMenuRef, closeEditorCtx} = useEditorContextMenu ({editorView , currentLanguage , canBlame })
14791423const openBlame = () => {
14801424 editorCtx .visible = false
14811425 const root = rootDir .value
@@ -1499,42 +1443,6 @@ const openFileHistory = () => {
14991443 const rel = path .slice (root .length ).replace (/ ^ [\\ /] / , ' ' )
15001444 fileHistory .value = {root , rel , name: rel .split (/ [\\ /] / ).pop () || rel }
15011445}
1502- const onEditorContext = async (e : MouseEvent ) => {
1503- const target = e .target as HTMLElement | null
1504- const lsp = lspSupportsLanguage (currentLanguage .value ) && !! editorView .value
1505- // 在编辑器内容区,且支持 LSP 或可 Blame 时弹出
1506- if (! target ?.closest (' .cm-content' ) || (! lsp && ! canBlame .value )) {
1507- return
1508- }
1509- editorCtx .lsp = lsp
1510- e .preventDefault ()
1511- const view = editorView .value
1512- if (view ) {
1513- const cur = view .state .selection .main
1514- const pos = view .posAtCoords ({x: e .clientX , y: e .clientY })
1515- // 仅在无选区、或右键点在选区之外时才移动光标;点在选区内则保留选区(不清除高亮)
1516- const insideSel = ! cur .empty && pos != null && pos >= cur .from && pos <= cur .to
1517- if (pos != null && ! insideSel ) {
1518- view .dispatch ({selection: {anchor: pos }})
1519- }
1520- }
1521- // 先按光标位置弹出,渲染后测量真实尺寸再夹取到视口内(菜单项数量可变,避免贴底/贴右裁切)
1522- editorCtx .x = e .clientX
1523- editorCtx .y = e .clientY
1524- editorCtx .visible = true
1525- await nextTick ()
1526- const el = editorMenuRef .value
1527- if (el ) {
1528- const r = el .getBoundingClientRect ()
1529- const margin = 8
1530- if (editorCtx .x + r .width > window .innerWidth ) {
1531- editorCtx .x = Math .max (margin , window .innerWidth - r .width - margin )
1532- }
1533- if (editorCtx .y + r .height > window .innerHeight ) {
1534- editorCtx .y = Math .max (margin , window .innerHeight - r .height - margin )
1535- }
1536- }
1537- }
15381446const runEditorCommand = (cmd : (v : any ) => boolean ) => {
15391447 closeEditorCtx ()
15401448 // 不在此处 focus 编辑器:重命名会弹出需要焦点的内联输入框
@@ -1648,51 +1556,8 @@ const openGit = () => {
16481556 showGit .value = true
16491557}
16501558
1651- // 文件树徽标用:绝对路径 → 状态字母(M/A/D/U)
1652- const gitStatus = ref <Record <string , string >>({})
1653- const gitRepo = ref (false )
1654- const gitBranch = ref (' ' )
1655- // 计算单个根的 Git 状态(路径用绝对路径作 key,便于多根合并到同一张表)
1656- const gitStatusFor = async (root : string ): Promise <{ isRepo: boolean , branch: string , map: Record <string , string > }> => {
1657- try {
1658- const s = await invoke <{ is_repo: boolean , branch: string , files: { path: string , index: string , worktree: string }[] }>(
1659- ' git_status' , {root }
1660- )
1661- const map: Record <string , string > = {}
1662- if (s .is_repo ) {
1663- for (const f of s .files ) {
1664- const code = f .index === ' ?'
1665- ? ' U'
1666- : (f .worktree .trim () || f .index .trim () || ' M' )
1667- map [` ${root }/${f .path } ` ] = code
1668- }
1669- }
1670- return {isRepo: s .is_repo , branch: s .branch || ' ' , map }
1671- }
1672- catch {
1673- return {isRepo: false , branch: ' ' , map: {}}
1674- }
1675- }
1676- const refreshGitStatus = async () => {
1677- if (! rootDir .value ) {
1678- gitStatus .value = {}
1679- gitRepo .value = false
1680- gitBranch .value = ' '
1681- return
1682- }
1683- const primary = await gitStatusFor (rootDir .value )
1684- gitRepo .value = primary .isRepo
1685- gitBranch .value = primary .branch
1686- const map: Record <string , string > = {... primary .map }
1687- // 额外挂载的根各自可为独立仓库,合并它们的状态徽标
1688- if (extraRoots .value .length ) {
1689- const extra = await Promise .all (extraRoots .value .map (er => gitStatusFor (er )))
1690- for (const e of extra ) Object .assign (map , e .map )
1691- }
1692- gitStatus .value = map
1693- // HEAD 可能因提交/切换分支变化,刷新编辑器行内差异基线
1694- fetchBaseline ()
1695- }
1559+ // 文件树 Git 徽标 + 当前分支(抽离到 useGitStatus);HEAD 变化时刷新差异基线
1560+ const {gitStatus, gitRepo, gitBranch, refreshGitStatus} = useGitStatus (rootDir , extraRoots , () => fetchBaseline ())
16961561
16971562// ===== 编辑器行内差异标记(vs HEAD)=====
16981563// 当前文件在 HEAD 中的内容;null 表示无基线(新文件/非 git/未跟踪),不显示标记
@@ -1803,6 +1668,9 @@ const {
18031668 loadConfig : loadEditorConfig
18041669} = useEditorConfig ()
18051670
1671+ // 标签会话持久化(依赖 editorConfig,放在其声明之后)
1672+ const {restoreSession} = useSessionTabs ({editorTabs , activeTabId , editorConfig , openPath , restorePinned , switchTab })
1673+
18061674// 强制刷新 CodeEditor 组件的 key
18071675const editorConfigKey = ref (0 )
18081676const consoleType = ref (' console' )
@@ -1912,44 +1780,8 @@ const handleSave = async () => {
19121780}
19131781
19141782// ===== 按文件记忆运行配置(args/stdin/env)=====
1915- const RUN_CONFIGS_KEY = ' run-configs'
1916- type RunConfig = { args: string , stdin: string , env: string }
1917- const loadRunConfigs = (): Record <string , RunConfig > =>
1918- kvGetJSON <Record <string , RunConfig >>(RUN_CONFIGS_KEY , {})
1919- // 把当前输入写入指定文件的配置(全空则删除该条)
1920- const saveRunConfig = (path : string ) => {
1921- const map = loadRunConfigs ()
1922- if (! runArgs .value && ! runStdin .value && ! runEnv .value ) {
1923- delete map [path ]
1924- }
1925- else {
1926- map [path ] = {args: runArgs .value , stdin: runStdin .value , env: runEnv .value }
1927- }
1928- kvSetJSON (RUN_CONFIGS_KEY , map )
1929- }
1930- // 载入指定文件的配置(无则清空)
1931- const loadRunConfig = (path : string | null ) => {
1932- const cfg = path ? loadRunConfigs ()[path ] : null
1933- runArgs .value = cfg ?.args || ' '
1934- runStdin .value = cfg ?.stdin || ' '
1935- runEnv .value = cfg ?.env || ' '
1936- }
1937-
1938- // 切换文件时:保存旧文件输入、载入新文件输入
1939- watch (currentFilePath , (np , op ) => {
1940- if (op ) {
1941- saveRunConfig (op )
1942- }
1943- loadRunConfig (np )
1944- })
1945-
1946- // 编辑输入时防抖保存到当前文件
1947- const persistRunConfig = debounce (() => {
1948- if (currentFilePath .value ) {
1949- saveRunConfig (currentFilePath .value )
1950- }
1951- }, 400 )
1952- watch ([runArgs , runStdin , runEnv ], () => persistRunConfig ())
1783+ // 按文件记忆运行输入(参数/stdin/环境变量)—— 抽离到 useRunConfig
1784+ useRunConfig (currentFilePath , runArgs , runStdin , runEnv )
19531785
19541786// 解析环境变量文本(KEY=值,按换行或分号分隔)
19551787const parseEnv = (text : string ): Record <string , string > => {
@@ -2407,7 +2239,6 @@ onMounted(async () => {
24072239 window .addEventListener (' keydown' , onGlobalKeydown , true )
24082240 window .addEventListener (' lsp:open-location' , onLspOpenLocation )
24092241 window .addEventListener (' lsp:code-actions' , onLspCodeActions )
2410- window .addEventListener (' contextmenu' , onEditorContext )
24112242
24122243 // 触发 app-ready 事件,通知主进程
24132244 window .dispatchEvent (new CustomEvent (' app-ready' ))
@@ -2418,6 +2249,5 @@ onUnmounted(() => {
24182249 window .removeEventListener (' keydown' , onGlobalKeydown , true )
24192250 window .removeEventListener (' lsp:open-location' , onLspOpenLocation )
24202251 window .removeEventListener (' lsp:code-actions' , onLspCodeActions )
2421- window .removeEventListener (' contextmenu' , onEditorContext )
24222252})
24232253 </script >
0 commit comments