331331 <!-- 文件夹内全局搜索 -->
332332 <SearchPanel v-if =" showSearch && rootDir " :root-dir =" rootDir " :extra-roots =" extraRoots " :scope =" searchScope " @open =" openSearchResult " @replaced =" reloadAffectedFiles " @close =" showSearch = false " />
333333
334+ <WorkspaceManager v-if =" showWorkspaces " :root-dir =" rootDir " :extra-roots =" extraRoots " @open =" openWorkspace " @close =" showWorkspaces = false " />
335+
334336 <!-- 快速打开文件 -->
335337 <QuickOpen v-if =" showQuickOpen && rootDir "
336338 :root-dir =" rootDir "
354356 :code =" code "
355357 :language =" currentLanguage "
356358 :current-line =" cursorInfo .line "
359+ :view =" editorView "
357360 @go =" gotoLine "
358361 @close =" showOutline = false " />
359362
463466 <button class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" aiCodeAction('explain')" >{{ t('aiCode.title.explain') }}</button >
464467 <button class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" aiCodeAction('refactor')" >{{ t('aiCode.title.refactor') }}</button >
465468 <button class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" aiCodeAction('test')" >{{ t('aiCode.title.test') }}</button >
469+ <button class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" aiCodeAction('doc')" >{{ t('aiCode.title.doc') }}</button >
466470 <button v-if =" canBlame || editorCtx.lsp" class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" aiFixDiagnostics" >{{ t('aiCode.title.fix') }}</button >
467471 <div class =" border-t border-gray-100 dark:border-gray-700 my-1" ></div >
468472 <button class =" w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer" @click =" sendToTerminal" >
@@ -544,6 +548,8 @@ import {useWorkspace} from './composables/useWorkspace'
544548import {useTextCommands } from ' ./composables/useTextCommands'
545549import {useBookmarks } from ' ./composables/useBookmarks'
546550import {foldAll , unfoldAll , matchBrackets } from ' @codemirror/language'
551+ import {selectParentSyntax } from ' @codemirror/commands'
552+ import {format as formatSql } from ' sql-formatter'
547553import {diagnostics } from ' ./editor/lspDiagnostics'
548554import {useGitPermalink } from ' ./composables/useGitPermalink'
549555import {useRevealInTree } from ' ./composables/useRevealInTree'
@@ -589,6 +595,7 @@ import {useDebug} from './composables/useDebug'
589595import AiAssistant from ' ./components/AiAssistant.vue'
590596import InlineGenerate from ' ./components/InlineGenerate.vue'
591597import SearchPanel from ' ./components/SearchPanel.vue'
598+ import WorkspaceManager from ' ./components/WorkspaceManager.vue'
592599import {useTheme , type AppTheme } from ' ./composables/useTheme'
593600import Modal from ' ./ui/Modal.vue'
594601import Button from ' ./ui/Button.vue'
@@ -749,10 +756,18 @@ const handleCopyRelativePath = (path: string) => {
749756const rootDir = ref <string | null >(null )
750757
751758// 远程仓库永久链接(复制 / 在浏览器打开)
752- const {copyPermalink, openPermalink} = useGitPermalink (rootDir , currentFilePath , cursorInfo )
759+ const {copyPermalink, openPermalink, openRepoOnWeb } = useGitPermalink (rootDir , currentFilePath , cursorInfo )
753760
754761// 多根工作区:额外挂载的文件夹(Git/搜索仍走主根 rootDir)
755- const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots} = useWorkspaceRoots (rootDir )
762+ const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots, setExtraRoots} = useWorkspaceRoots (rootDir )
763+
764+ // 命名工作区:保存/打开一组根
765+ const showWorkspaces = ref (false )
766+ const openWorkspace = (ws : {rootDir: string ; extraRoots: string []}) => {
767+ showWorkspaces .value = false
768+ openFolderPath (ws .rootDir ) // 设主根并清空额外根
769+ setExtraRoots (ws .extraRoots ) // 再恢复该工作区的额外根
770+ }
756771const sidebarVisible = ref (kvGet (' sidebar-visible' ) === ' true' )
757772// 专注模式:隐藏顶部工具栏/运行输入/侧栏/状态栏,沉浸编辑
758773const zenMode = ref (false )
@@ -1175,6 +1190,65 @@ const convertIndentation = (toTabs: boolean) => {
11751190 toast .success (t (' app.indentConverted' ))
11761191}
11771192
1193+ // 展开/收缩选区:按语法节点逐级扩选,收缩用选区栈回退
1194+ let expandStack: { anchor: number , head: number }[] = []
1195+ let expandLastKey = ' '
1196+ const selKey = (view : any ) => {
1197+ const s = view .state .selection .main
1198+ return s .anchor + ' :' + s .head
1199+ }
1200+ const expandSelection = () => {
1201+ const view = editorView .value
1202+ if (! view ) {
1203+ return
1204+ }
1205+ // 用户手动改过选区则重置栈
1206+ if (selKey (view ) !== expandLastKey ) {
1207+ expandStack = []
1208+ }
1209+ const s = view .state .selection .main
1210+ expandStack .push ({anchor: s .anchor , head: s .head })
1211+ selectParentSyntax (view )
1212+ view .focus ()
1213+ expandLastKey = selKey (view )
1214+ }
1215+ const shrinkSelection = () => {
1216+ const view = editorView .value
1217+ if (! view ) {
1218+ return
1219+ }
1220+ const prev = expandStack .pop ()
1221+ if (prev ) {
1222+ view .dispatch ({selection: {anchor: prev .anchor , head: prev .head }})
1223+ view .focus ()
1224+ expandLastKey = selKey (view )
1225+ }
1226+ }
1227+
1228+ // 格式化 SQL(选区或全文)
1229+ const formatSqlDoc = () => {
1230+ const view = editorView .value
1231+ if (! view ) {
1232+ return
1233+ }
1234+ if (currentLanguage .value !== ' sql' ) {
1235+ toast .info (t (' app.sqlFormatOnlySql' ))
1236+ return
1237+ }
1238+ const sel = view .state .selection .main
1239+ const from = sel .empty ? 0 : view .state .doc .lineAt (sel .from ).from
1240+ const to = sel .empty ? view .state .doc .length : view .state .doc .lineAt (sel .to ).to
1241+ try {
1242+ const out = formatSql (view .state .doc .sliceString (from , to ), {language: ' sql' , keywordCase: ' upper' })
1243+ view .dispatch ({changes: {from , to , insert: out }, selection: {anchor: from }})
1244+ view .focus ()
1245+ toast .success (t (' app.sqlFormatted' ))
1246+ }
1247+ catch (error ) {
1248+ toast .error (t (' app.sqlFormatFailed' ) + error )
1249+ }
1250+ }
1251+
11781252// 转到匹配括号:取光标前后的括号,跳到其配对处
11791253const goToMatchingBracket = () => {
11801254 const view = editorView .value
@@ -1422,7 +1496,7 @@ const runTests = async () => {
14221496}
14231497
14241498// C2:对选区(无选区则整篇)执行 AI 操作:解释 / 重构 / 生成测试
1425- const aiCodeCtx = ref <{action: ' explain' | ' refactor' | ' test' | ' fix' ; code: string ; from: number ; to: number ; diagnostics? : string } | null >(null )
1499+ const aiCodeCtx = ref <{action: ' explain' | ' refactor' | ' test' | ' fix' | ' doc ' ; code: string ; from: number ; to: number ; diagnostics? : string } | null >(null )
14261500// AI 修复诊断:把当前文件的 LSP 诊断交给 AI 修复整篇
14271501const aiFixDiagnostics = () => {
14281502 closeEditorCtx ()
@@ -1437,7 +1511,7 @@ const aiFixDiagnostics = () => {
14371511 const diagText = diagnostics .value .map (d => ` [${d .severity }] L${d .line }:${d .col } ${d .message } ` ).join (' \n ' )
14381512 aiCodeCtx .value = {action: ' fix' , code: view .state .doc .toString (), from: 0 , to: view .state .doc .length , diagnostics: diagText }
14391513}
1440- const aiCodeAction = (action : ' explain' | ' refactor' | ' test' ) => {
1514+ const aiCodeAction = (action : ' explain' | ' refactor' | ' test' | ' doc ' ) => {
14411515 closeEditorCtx ()
14421516 const view = editorView .value
14431517 if (! view ) {
@@ -1468,7 +1542,17 @@ const onAiReplace = (code: string) => {
14681542const onAiInsert = (code : string ) => {
14691543 const view = editorView .value
14701544 const ctx = aiCodeCtx .value
1471- if (view && ctx ) {
1545+ if (! view || ! ctx ) {
1546+ return
1547+ }
1548+ if (ctx .action === ' doc' ) {
1549+ // 文档注释插入到目标代码所在行之前,缩进对齐
1550+ const line = view .state .doc .lineAt (ctx .from )
1551+ const indent = line .text .match (/ ^ \s * / )?.[0 ] ?? ' '
1552+ const commented = code .trimEnd ().split (' \n ' ).map (l => indent + l ).join (' \n ' )
1553+ view .dispatch ({changes: {from: line .from , insert: ` ${commented }\n ` }})
1554+ }
1555+ else {
14721556 view .dispatch ({changes: {from: ctx .to , insert: ` \n\n ${code }\n ` }})
14731557 }
14741558}
@@ -2170,7 +2254,7 @@ const isOverlayOpen = () =>
21702254 || showHistory .value || showViewer .value || showRunPrompt .value
21712255 || showQuickOpen .value || showGenerate .value || showSearch .value
21722256 || showCommandPalette .value || showDiff .value || showGoToLine .value || showOutline .value || showSnippets .value
2173- || applyPreview .value != null || clipboardDiff .value != null
2257+ || applyPreview .value != null || clipboardDiff .value != null || showWorkspaces . value
21742258
21752259// 全局快捷键(绑定可在设置中自定义)
21762260const {matchAction : matchShortcut, reload : reloadShortcuts, getBinding, formatCombo} = useShortcuts ()
@@ -2193,7 +2277,9 @@ const shortcutDispatch: Record<string, () => void> = {
21932277 toggleSidebar : () => toggleSidebar (),
21942278 toggleTerminal : () => toggleTerminal (),
21952279 toggleWordWrap : () => toggleWordWrap (),
2196- toggleBookmark : () => toggleBookmark ()
2280+ toggleBookmark : () => toggleBookmark (),
2281+ expandSelection : () => expandSelection (),
2282+ shrinkSelection : () => shrinkSelection ()
21972283}
21982284
21992285// 切换自动换行(即时生效并随编辑器配置持久化)
@@ -2228,6 +2314,7 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22282314 {id: ' formatOnSave' , label: formatOnSave .value ? t (' command.formatOnSaveOff' ) : t (' command.formatOnSaveOn' ), icon: Save , run : () => toggleFormatOnSave ()},
22292315 {id: ' open' , label: t (' command.open' ), icon: FolderOpen , hint: hintOf (' open' ), run : () => handleOpenFileClick ()},
22302316 {id: ' openFolder' , label: t (' command.openFolder' ), icon: FolderOpen , run : () => openFolder ()},
2317+ {id: ' workspaces' , label: t (' command.workspaces' ), icon: FolderOpen , run : () => { showWorkspaces .value = true }},
22312318 {id: ' save' , label: t (' command.save' ), icon: Save , hint: hintOf (' save' ), run : () => handleSave ()},
22322319 {id: ' saveAs' , label: t (' command.saveAs' ), icon: Save , hint: hintOf (' saveAs' ), run : () => saveFileAs ()},
22332320 {id: ' newTab' , label: t (' command.newTab' ), icon: Plus , hint: hintOf (' newTab' ), run : () => handleNewTab ()},
@@ -2245,6 +2332,7 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22452332 {id: ' generateTests' , label: t (' command.generateTests' ), icon: Sparkles , run : () => generateTests ()},
22462333 {id: ' formatWithAi' , label: t (' command.formatWithAi' ), icon: Sparkles , run : () => formatWithAi ()},
22472334 {id: ' aiFixDiagnostics' , label: t (' command.aiFixDiagnostics' ), icon: Sparkles , run : () => aiFixDiagnostics ()},
2335+ {id: ' aiGenDoc' , label: t (' command.aiGenDoc' ), icon: Sparkles , run : () => aiCodeAction (' doc' )},
22482336 {id: ' history' , label: t (' command.history' ), icon: History , run : () => { showHistory .value = true }},
22492337 {id: ' diff' , label: t (' command.diff' ), icon: GitCompare , run : () => openDiff ()},
22502338 {id: ' compareClipboard' , label: t (' command.compareClipboard' ), icon: GitCompare , run : () => compareWithClipboard ()},
@@ -2258,6 +2346,7 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22582346 {id: ' revealInTree' , label: t (' command.revealInTree' ), icon: FolderOpen , run : () => revealInTree ()},
22592347 {id: ' copyPermalink' , label: t (' command.copyPermalink' ), icon: GitBranch , run : () => copyPermalink ()},
22602348 {id: ' openPermalink' , label: t (' command.openPermalink' ), icon: GitBranch , run : () => openPermalink ()},
2349+ {id: ' openRepoOnWeb' , label: t (' command.openRepoOnWeb' ), icon: GitBranch , run : () => openRepoOnWeb ()},
22612350 {id: ' sortLinesAsc' , label: t (' command.sortLinesAsc' ), group: t (' command.groupText' ), icon: ArrowDownAZ , run : () => sortLines (false )},
22622351 {id: ' sortLinesDesc' , label: t (' command.sortLinesDesc' ), group: t (' command.groupText' ), icon: ArrowUpAZ , run : () => sortLines (true )},
22632352 {id: ' toUpperCase' , label: t (' command.toUpperCase' ), group: t (' command.groupText' ), icon: CaseUpper , run : () => transformSelectionOrLine (s => s .toUpperCase ())},
@@ -2270,6 +2359,9 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22702359 {id: ' foldAll' , label: t (' command.foldAll' ), group: t (' command.groupCode' ), icon: FoldVertical , run : () => { if (editorView .value ) foldAll (editorView .value ) }},
22712360 {id: ' unfoldAll' , label: t (' command.unfoldAll' ), group: t (' command.groupCode' ), icon: UnfoldVertical , run : () => { if (editorView .value ) unfoldAll (editorView .value ) }},
22722361 {id: ' goToMatchingBracket' , label: t (' command.goToMatchingBracket' ), group: t (' command.groupCode' ), icon: Code2 , run : () => goToMatchingBracket ()},
2362+ {id: ' formatSql' , label: t (' command.formatSql' ), group: t (' command.groupCode' ), icon: Code2 , run : () => formatSqlDoc ()},
2363+ {id: ' expandSelection' , label: t (' command.expandSelection' ), group: t (' command.groupCode' ), icon: Code2 , hint: hintOf (' expandSelection' ), run : () => expandSelection ()},
2364+ {id: ' shrinkSelection' , label: t (' command.shrinkSelection' ), group: t (' command.groupCode' ), icon: Code2 , hint: hintOf (' shrinkSelection' ), run : () => shrinkSelection ()},
22732365 {id: ' toggleBookmark' , label: t (' command.toggleBookmark' ), group: t (' command.groupBookmark' ), icon: Bookmark , hint: hintOf (' toggleBookmark' ), run : () => toggleBookmark ()},
22742366 {id: ' nextBookmark' , label: t (' command.nextBookmark' ), group: t (' command.groupBookmark' ), icon: Bookmark , run : () => nextBookmark ()},
22752367 {id: ' prevBookmark' , label: t (' command.prevBookmark' ), group: t (' command.groupBookmark' ), icon: Bookmark , run : () => prevBookmark ()},
0 commit comments