From aa23f356d983675dc4a0da23c6dd02417dc4d0ab Mon Sep 17 00:00:00 2001 From: choiminyeong Date: Mon, 28 Sep 2026 16:17:22 +0900 Subject: [PATCH] TASK-059: jump from a function in the outline to its counterpart file Co-Authored-By: Claude Fable 5.1 --- .../feature/codebrowser/CodeViewerScreen.kt | 53 ++++++++++++++++- docs/UBGitea_기술소개.pptx | Bin 725853 -> 725853 bytes docs/development-log.md | 9 +++ ...059-jump-from-declaration-to-definition.md | 55 ++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tasks/059-jump-from-declaration-to-definition.md diff --git a/app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/CodeViewerScreen.kt b/app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/CodeViewerScreen.kt index acbd407..f27f54a 100644 --- a/app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/CodeViewerScreen.kt +++ b/app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/CodeViewerScreen.kt @@ -202,6 +202,8 @@ fun CodeViewerScreen( // The share of the width the left pane of a header/source pair takes. var splitFraction by rememberSaveable { mutableFloatStateOf(DEFAULT_SPLIT) } val drawerState = rememberDrawerState(DrawerValue.Closed) + // The sheet is shown from outside the layout below, where the jump is worked out. + var jumpForSheet by remember { mutableStateOf(null) } // The other half of a header/source pair, once a wide window has asked for it and it has // arrived as text. @@ -265,6 +267,27 @@ fun CodeViewerScreen( val sidePaneIsShown = layout == ViewerLayout.WIDE && !showsPair val drawerWidth = (maxWidth * DRAWER_WIDTH_FRACTION).coerceAtMost(MaxDrawerWidth) + // From a function in the outline to the same function in the other file of the pair: from + // a declaration in the header to its definition in the source, and back. Beside each other + // they already move together, so there it is not offered. + val jump = if (counterpartPath == null || showsPair) { + null + } else { + SymbolJump( + label = "." + counterpartPath.value.substringAfterLast('.'), + description = stringResource( + if (cppRoleOf(counterpartPath) == CppFileRole.HEADER) R.string.codeviewer_jump_to_header else R.string.codeviewer_jump_to_source, + counterpartPath.value.substringAfterLast('/'), + ), + onJump = { symbol -> + outlineOpen = false + scope.launch { drawerState.close() } + onOpenSibling(counterpartPath, symbol.name) + }, + ) + } + jumpForSheet = jump + val tools = ViewerTools( preferences = preferences, searching = searching, @@ -310,6 +333,7 @@ fun CodeViewerScreen( onOpenEntry = onOpenEntry, onToggleFolder = onToggleFolder, outlineFirst = outlineFirst, + jump = jump, onSwap = swapPanes, onSymbolSelected = { scope.launch { drawerState.close() } @@ -384,6 +408,7 @@ fun CodeViewerScreen( onOpenEntry = onOpenEntry, onToggleFolder = onToggleFolder, outlineFirst = outlineFirst, + jump = jump, onSwap = swapPanes, onSymbolSelected = { goToSymbol(it) }, modifier = Modifier.width(sidePaneWidth).fillMaxHeight(), @@ -421,6 +446,7 @@ fun CodeViewerScreen( if (outlineOpen) { OutlineSheet( document = document, + jump = jumpForSheet, currentLine = firstVisibleLine, collapsed = collapsed, onToggleCollapsed = toggleCollapsed, @@ -437,6 +463,9 @@ fun CodeViewerScreen( } } +/** Goes from a function to the function of the same name in the other file of a header/source pair. */ +private class SymbolJump(val label: String, val description: String, val onJump: (CodeSymbol) -> Unit) + private class ViewerTools( val preferences: CodeViewerPreferences, val searching: Boolean, @@ -997,13 +1026,14 @@ private fun SidePane( onSymbolSelected: (CodeSymbol) -> Unit, outlineFirst: Boolean, onSwap: () -> Unit, + jump: SymbolJump?, modifier: Modifier = Modifier, ) { val outline = remember(document.symbols, collapsed) { outlineTree(document.symbols, collapsed) } val rows = remember(state.tree) { state.tree.rows() } val folders: @Composable (Modifier) -> Unit = { FolderCard(state, rows, onOpenFile, onToggleFolder, onOpenEntry, it) } val symbols: @Composable (Modifier) -> Unit = { - SymbolCard(document, outline, currentLine, collapsed, onToggleCollapsed, onSymbolSelected, it) + SymbolCard(document, outline, currentLine, collapsed, onToggleCollapsed, onSymbolSelected, jump, it) } Column(modifier.background(MaterialTheme.colorScheme.background).padding(horizontal = 10.dp, vertical = 10.dp)) { val half = Modifier.weight(1f).fillMaxWidth() @@ -1065,6 +1095,7 @@ private fun SymbolCard( collapsed: Set, onToggleCollapsed: (CodeSymbol) -> Unit, onSymbolSelected: (CodeSymbol) -> Unit, + jump: SymbolJump?, modifier: Modifier, ) { val current = currentIn(outline, document.symbols, currentLine) @@ -1092,6 +1123,7 @@ private fun SymbolCard( isCollapsed = symbol.line in collapsed, methodCount = methodCountOf(document.symbols, symbol), onToggle = { onToggleCollapsed(symbol) }, + jump = jump, ) { onSymbolSelected(symbol) } } } @@ -1246,6 +1278,7 @@ private fun SymbolRow( isCollapsed: Boolean, methodCount: Int, onToggle: () -> Unit, + jump: SymbolJump?, onClick: () -> Unit, ) { val colors = rememberCodeSyntaxColors() @@ -1311,6 +1344,22 @@ private fun SymbolRow( maxLines = 1, modifier = Modifier.padding(end = if (compact) 8.dp else 0.dp), ) + } else if (jump != null && symbol.kind == SymbolKind.METHOD) { + Surface( + onClick = { jump.onJump(symbol) }, + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.semantics { contentDescription = jump.description }, + ) { + Row( + Modifier.padding(start = 8.dp, end = 4.dp, top = 5.dp, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(jump.label, fontFamily = FontFamily.Monospace, fontSize = 12.sp, fontWeight = FontWeight.Bold, maxLines = 1) + UBIcon(R.drawable.ic_ub_chevron_right, null, size = 14.dp) + } + } } else if (!compact) { Text( stringResource(R.string.codeviewer_outline_line, symbol.line + 1), @@ -1326,6 +1375,7 @@ private fun SymbolRow( @Composable private fun OutlineSheet( document: CodeDocument, + jump: SymbolJump?, currentLine: Int, collapsed: Set, onToggleCollapsed: (CodeSymbol) -> Unit, @@ -1400,6 +1450,7 @@ private fun OutlineSheet( isCollapsed = symbol.line in collapsed, methodCount = methodCountOf(document.symbols, symbol), onToggle = { onToggleCollapsed(symbol) }, + jump = jump, ) { onSymbolSelected(symbol) } } } diff --git a/docs/UBGitea_기술소개.pptx b/docs/UBGitea_기술소개.pptx index 6eef4fc53daf3641e3a271659c87e895f387179d..005fa20d97a5a65c826e82656936d13b58564622 100644 GIT binary patch delta 2913 zcmY*aeN*z2rHH~XgD3oUA@(XOH8MSyI^3Ch+y-%$bf4qIqIluk; z{r1`SZmzxWzV^O5#)9B4Cb=wRtZeVS3iqp+T3t#3xb zx_6Q|_ohk!u#Q~bXQT*tyd}B#U%FY?&)5;r^dn#@a8yTn-LjLCV<8mE6KECbt(8H> zqKfohl}Ey3pX`P(QJG=|-wI_Y24mb#8DeXEon4PfPF;Cz+^K2452pV?Ry|3{Q#{^; zlqbd0?vw;T)~AdQhxpACgHWUI_J$3(&MBTRS!?gXl5b%9_phu}5Vrj;3ViXqSEVoz za4H3PnXaIqOczhTBt`nR>{{jmU~^Tuj1J-|E6ezePI-dw%|iz@+&Y__ZsYz^+3j0< zO+n*sfMQa&~{J0p$9i6P5jej@1aQ%GD<$l-Kt$##74G zB(wRD{;I^A)=JPj)4x{2T=O3rB}_J33Z*T-fuqbuiIKl8&m9P$Ki*j=rJJKa*A3Nd zYLOO6s8avL9}V=%D+d@e^S_WTfh{xflC*aTBHGo&gXqJ&8zZAjECVTfpy>3iwc&+_Vbvn*1_Df-9XC1_?^mJ#-o!Riqk>2mEr9 z=}3@YlDu)@8h?909tC-S8<3@Zc=-ErMHKo>#F^_<=qSl5v3G@1NrXk+*@uzPJhxiG zewc^XDlW3?{2*;S*}eByg{o^gtx$FE)hkrpiGM25>p-n#qAdjN-iWa^LEFru9@{V( z-;`a$@0}^$78F#xjThC~qRjtHwe8u46*ZjVDLgb^8)v?A($cr4JL#ccUfD2M6V><>1y6cwKtJ&Mnq;5>F#~ZPP&`s*#ET$>&&}X zzn(5WzuDeKFMMTxl!^toMFSom7Bok-?fu}CoYm+qC9%-E~;k37;8@0jvQ{P<#d z9(Q~!uQwY*^*_OYGz$}T@>7zeQ>(JlbZS+>Y~4oR@u_vIO2NcfbKfETHjy9cFq6Q< z1hc9}uck`c7G<0R6K?Z`TKyI_VA)Z<9;TK#{DRJ@b<<~*v{UBw7Aq4DcYGx@-}+j= z7!5lcdvy(8s^|OlG8m_(eR%L@q%3|o_~QRzCr4$M;ep4rGnlbFznvvQtY~NP5I1(R z5xlLPJq(LLJ4=FC)M0Hd_Omgt*y?8~KOvj_ghYhiL5O1jpVz_Qwjg&VL~HXgh!*Dv z?&xGNPl-6QlhQ2dX+Lc?h)v`QbQ0IZ`z#d@Pv2)55RcqvvmoBNPd>+Yu~cj${Da`@ zyJ)w%i)8`oyDl~xV*CS&n;(#~6Ay@`L##dBYysf5ce7;>zwTz~5J&c~`4Bhsuq;%; ziC-|lBKgH0mJCOZUY3qWIPpD0*3qV3y4dPo;i8u<1?b#90mX@r(3(CLkJ(v9$tV2T z9xJ=Ek4?s{86kOBIBL!AC#{lxvQ^(twn7I;VEF(EoE#7WILcALIVc2fpU$050;>ku zWU#Y;kg~oVB-TkoY(5GPYkm1cf>sO(FEdNZza%fML*ykoKv};Hke6ct@-h%W){4aj zUCXqOTs0i5RQL=k73VjWDoF;Fijx~lT__oNt$QQ`JJ2K<*a4SpPzOABKmIp4*(npW zK{hhb_kUYXp5ax75y?j@h6lH@PY?crr|Z_%CdI(Fy;?DHq53|>z!xgbW?<|0+DPcC zjm{HP!TRmU-K&YUTotS_zjDnc);3iXHTS8V5AjA0vi7XOahLoaF1JdQgSqeLU|mm!aX%I$XHb2o-^`rc?!#fwx3uGfzn&AmCIA@-kfk0hunI zeo_keuHCZ01;D1tT{1d|si-L9=R4$a-k0|7(Qxa-_6|IH z?$+o7Ji9>}?Tzki!6RqaR~kUh4LDKR=Wkzv(8^qWB0_mxFJnAqmzroc?bV->c;hk& zdT08UNtkPXyh_4kv$;@OdmkKSHb{*8t$k#-AN}!;LMh!G`JHa4W@EE7S3;G#J6|-= zE3fEh%*_8@ItjMSgp<RyvsAzb%jCwzkk{yw84{fCurNqaviydl=%_r^VBGJM6HQ0F{DkC*3Dx*3 z{qhLN`=DQz^5Nm5dlgaWLt)3xQK2IwtHhp#N(B)Xb!G2ELi6|%1^Z#{Tc)_kuJi4* zv1Iqk`wCUpd{m+8UawQAy7xX&BA0_&^LSej+C3j-YlOC$2jXl)WPDS$483x!ctb!y z@djR0Yl|>{nPl6x0V}FM!c%x~zBa~u^{}lKuXp1Sk)Qpcl@CNqYi&sn!o%xhRMM!q zNhOV9w@R3uF>2{Tz>Io9U?y%Cm_d()K_632RL9TH3kaB>M?NOMtS-%iq}_j8NoZ(G zMQklEOSRu@p7N=cbW5A2o_&*Va;wp1O}BahP44Jb*DZled%h+zBXatl#7urD(7NQA z1=_2WUOzUcfzl(EYn0xzLQBW=)UW?dzhM0EI|;A*MiatH;1^F|CQmc0hOhow6AkCu zDGers8FNaTjYR8{?Q}ISq}m%vHzO=Ff^@e}x0CMb+wGrk!#eY>)UBY4&u_A~(hFbF z7rvV;6n$qG7Un!1X(y7~Pg^9IoAT(TMP~FF`=@dI{^>S1KQvFC$sM1`E6j#q{SBCp zW?_O(c1jX;>Qq*mPMs>4rrYQxd?%1neBJTa|W+HePXI570Ra8jp+>GPk z!EHWPqhG}PUvNOLgP~;({Z{AHxun}l+9>l}vy};ln|>6UFaM;Uh=iS0J-UYP)T4cR z8B9~tJ~VI*QWoDBc;XM($x+!Uc;HcO3`Q)^Z(|7%%iCBi#8n+^7;kN3H^Rc-#u6bG zwOgC>d~6gf*7;bfnU(i!ug4AmJCOZ z9+r+rIPl{Jt)taFbg@-E!bK0e2cR>01r!H9LaTdOEM{jJC13D|yRGbwUN#Z8ZVSpg z%~5MwA8D2Jk*&HuvK8D<0{8Zlz~O!&fRh{roC8AO($U-`5?DOICW4)v1C;gK0I^OO zWV29sNXwJ&6103!c$reN>j(1EGDuz`{gidTpS&FOlb3!!vX;*?=vpR!>Z;~orQB;! zsW`r|R7oKw_yYuzRp*nvjLzz(=%gE|m*`Li#{$!3|L^|FzHzJF~#e2iBb zMmQg-7;(6jeRSYDo~~P4s}%#^_A155h3b1117D~#n}Mz0ZX=;HHad@21?z9aw=N;p zU8-P>y2Uk(SX)(5)Qkr<-@q^0ETK5fz>lR+69nHrUeiJZ-)Kf0XiIj|%C?i%K0A52 zY$q=%xx8fO%0|VJREG~ zA~%d)-V|)$Cms?)juwTGqjy8dku6kk