detached 4ca17b2

Fix popup root split resizing and focus handling

commit
4ca17b2843ef2338ea03e9633845220bf01cfa9b
author
bamen · · 2025-10-12 21:09 UTC
committer
bamen · · 2025-10-12 18:16 UTC
parent
d244331
signature
unsigned
3 files · +80 -61
M internal/app/browser/focus_state_machine.go +11 -2
@@ -709,8 +709,17 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
709709 log.Printf("[FSM] Processing focus request %s: %s -> %p",
710710 request.ID, request.Source, request.TargetNode)
711711
712- // Execute focus change
713- if err := fsm.executeFocusChange(request); err != nil {
712+ // GTK operations must run on main thread - always use IdleAdd for simplicity
713+ var err error
714+ done := make(chan struct{})
715+ _ = webkit.IdleAdd(func() bool {
716+ err = fsm.executeFocusChange(request)
717+ close(done)
718+ return false
719+ })
720+ <-done
721+
722+ if err != nil {
714723 fsm.mu.Lock()
715724 fsm.metrics.FailedRequests++
716725 fsm.mu.Unlock()
@@ -709,8 +709,17 @@ func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
709 log.Printf("[FSM] Processing focus request %s: %s -> %p",709 log.Printf("[FSM] Processing focus request %s: %s -> %p",
710 request.ID, request.Source, request.TargetNode)710 request.ID, request.Source, request.TargetNode)
711 711
712- // Execute focus change712+ // GTK operations must run on main thread - always use IdleAdd for simplicity
713- if err := fsm.executeFocusChange(request); err != nil {713+ var err error
714+ done := make(chan struct{})
715+ _ = webkit.IdleAdd(func() bool {
716+ err = fsm.executeFocusChange(request)
717+ close(done)
718+ return false
719+ })
720+ <-done
721+
722+ if err != nil {
714 fsm.mu.Lock()723 fsm.mu.Lock()
715 fsm.metrics.FailedRequests++724 fsm.metrics.FailedRequests++
716 fsm.mu.Unlock()725 fsm.mu.Unlock()
M internal/app/browser/workspace_pane_ops.go +67 -55
@@ -393,18 +393,10 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
393393 }
394394 paned.SetHExpand(true)
395395 paned.SetVExpand(true)
396-
397- // Configure resize behavior based on whether this is a popup
398- if existingPane != nil {
399- // For popup panes: preserve existing pane size, let popup take remaining space
400- paned.SetResizeStartChild(false) // existing pane keeps its size
401- paned.SetResizeEndChild(true) // popup can resize
402- log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible")
403- } else {
404- // For normal splits: both panes can resize
405- paned.SetResizeStartChild(true)
406- paned.SetResizeEndChild(true)
407- }
396+
397+ // Allow both children to resize; avoids shrinking existing pane to zero during root splits
398+ paned.SetResizeStartChild(true)
399+ paned.SetResizeEndChild(true)
408400
409401 newLeaf := &paneNode{
410402 pane: newPane,
@@ -422,25 +414,21 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
422414 parent := split.parent
423415
424416 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption
417+ // NOTE: The active class is correctly removed here - focus will transfer to the NEW pane
418+ // The focus state machine will apply the active class to the new pane via SetActivePane()
425419 if splitTargetContainer != nil {
426420 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)
427421 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {
428422 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)
423+ log.Printf("[workspace] removed active class from split target (will transfer to new pane)")
429424 }
430425 }
431426
432427 // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented.
433428 wm.safelyDetachControllersBeforeReparent(splitTarget)
434429
435- // Detach split target container from its current GTK parent before inserting into new paned.
436- if parent == nil {
437- // Split target is the root - remove it from the window
438- log.Printf("[workspace] removing split target container=%p from window", splitTargetContainer)
439- if wm.window != nil {
440- wm.window.SetChild(nil)
441- }
442- // GTK4 will auto-unparent when we add to new paned - no manual unparent needed
443- } else if parent.container != nil {
430+ // For non-root splits, unparent from parent paned
431+ if parent != nil && parent.container != nil {
444432 // Split target has a parent paned - remove it (automatically unparents in GTK4)
445433 log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container)
446434 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {
@@ -453,6 +441,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
453441 webkit.WidgetQueueAllocate(parent.container)
454442 }
455443 }
444+ // NOTE: For root splits (parent == nil), we DON'T unparent here.
445+ // We'll unparent immediately before adding to paned below to minimize orphan time.
456446
457447 // Set up the tree structure first
458448 if existingFirst {
@@ -479,29 +469,48 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
479469
480470 // GTK4 handles widget operations automatically - no need to force redraw
481471
482- // Add both containers to the new paned
483- if existingFirst {
484- if splitTargetContainer != nil {
472+ // Handle root split: attach paned to window FIRST, then let GTK handle reparenting
473+ if parent == nil {
474+ // ROOT SPLIT: Attach empty paned to window, then add children
475+ // GTK will automatically reparent the existing widget when we call SetStartChild/SetEndChild
476+ if wm.window != nil {
477+ // Clear any existing window child (the splitTargetContainer)
478+ wm.window.SetChild(nil)
479+
480+ // Attach the new paned to window FIRST
481+ wm.window.SetChild(paned)
482+ log.Printf("[workspace] ROOT SPLIT: attached paned %p to window", paned)
483+ }
484+
485+ // Now add children to the paned - GTK will handle reparenting automatically
486+ if existingFirst {
485487 paned.SetStartChild(splitTargetContainer)
486488 paned.SetEndChild(newContainer)
487- log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer)
488- }
489- } else {
490- if splitTargetContainer != nil {
489+ log.Printf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer)
490+ } else {
491491 paned.SetStartChild(newContainer)
492492 paned.SetEndChild(splitTargetContainer)
493- log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer)
494- }
495- }
496-
497- // Attach the new paned to its parent
498- if parent == nil {
499- if wm.window != nil {
500- wm.window.SetChild(paned)
501- paned.QueueAllocate()
502- log.Printf("[workspace] paned set as window child: paned=%p", paned)
493+ log.Printf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer)
503494 }
504495 } else {
496+ // NON-ROOT SPLIT: Standard approach
497+
498+ // Add both containers to the new paned
499+ if existingFirst {
500+ if splitTargetContainer != nil {
501+ paned.SetStartChild(splitTargetContainer)
502+ paned.SetEndChild(newContainer)
503+ log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer)
504+ }
505+ } else {
506+ if splitTargetContainer != nil {
507+ paned.SetStartChild(newContainer)
508+ paned.SetEndChild(splitTargetContainer)
509+ log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer)
510+ }
511+ }
512+
513+ // Attach the new paned to its parent
505514 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {
506515 if parent.left == split {
507516 parentPaned.SetStartChild(paned)
@@ -519,22 +528,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
519528 // Synchronize the divider once the paned has a meaningful allocation.
520529 wm.syncPanedDivider(paned, orientation, split)
521530
522- // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting
523- // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues
524- if target != nil && target.container != nil {
525- wm.scheduleIdleGuarded(func() bool {
526- if target == nil || !target.widgetValid || target.container == nil {
527- return false
528- }
529- // Show widget to reconnect WebKit rendering pipeline
530- webkit.WidgetShow(target.container)
531- // Force GTK to recalculate size and recreate rendering surface
532- webkit.WidgetQueueResize(target.container)
533- webkit.WidgetQueueDraw(target.container)
534- log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting")
535- return false
536- }, target)
537- }
531+ // GTK4 automatically handles widget rendering after reparenting - no manual updates needed
538532
539533 wm.viewToNode[newPane.webView] = newLeaf
540534 wm.ensureHover(newLeaf)
@@ -555,7 +549,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
555549 // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow
556550 log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)")
557551 }
558-
552+
559553 if newPane.zoomController != nil {
560554 newPane.zoomController.ApplyInitialZoom()
561555 }
@@ -574,6 +568,24 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
574568 if newContainer != nil {
575569 webkit.WidgetShow(newContainer)
576570 }
571+
572+ // CRITICAL: For root splits, also show the split target container after reparenting
573+ // When the split target is reparented from window → paned, GTK doesn't preserve visibility
574+ if parent == nil && splitTargetContainer != nil {
575+ webkit.WidgetShow(splitTargetContainer)
576+ webkit.WidgetQueueAllocate(splitTargetContainer)
577+ webkit.WidgetQueueDraw(splitTargetContainer)
578+ }
579+
580+ // Ensure the underlying WebView widget is visible after reparenting
581+ if parent == nil && splitTarget != nil && splitTarget.pane != nil && splitTarget.pane.webView != nil {
582+ if webViewWidget := splitTarget.pane.webView.AsWidget(); webViewWidget != nil {
583+ webkit.WidgetShow(webViewWidget)
584+ webkit.WidgetQueueAllocate(webViewWidget)
585+ webkit.WidgetQueueDraw(webViewWidget)
586+ }
587+ }
588+
577589 // Attach GTK focus controller to new pane
578590 if wm.focusStateMachine != nil {
579591 wm.focusStateMachine.attachGTKController(newLeaf)
@@ -393,18 +393,10 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
393 }393 }
394 paned.SetHExpand(true)394 paned.SetHExpand(true)
395 paned.SetVExpand(true)395 paned.SetVExpand(true)
396- 396+
397- // Configure resize behavior based on whether this is a popup397+ // Allow both children to resize; avoids shrinking existing pane to zero during root splits
398- if existingPane != nil {398+ paned.SetResizeStartChild(true)
399- // For popup panes: preserve existing pane size, let popup take remaining space399+ paned.SetResizeEndChild(true)
400- paned.SetResizeStartChild(false) // existing pane keeps its size
401- paned.SetResizeEndChild(true) // popup can resize
402- log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible")
403- } else {
404- // For normal splits: both panes can resize
405- paned.SetResizeStartChild(true)
406- paned.SetResizeEndChild(true)
407- }
408 400
409 newLeaf := &paneNode{401 newLeaf := &paneNode{
410 pane: newPane,402 pane: newPane,
@@ -422,25 +414,21 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
422 parent := split.parent414 parent := split.parent
423 415
424 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption416 // Clear CSS classes before reparenting to avoid GTK bloom filter corruption
417+ // NOTE: The active class is correctly removed here - focus will transfer to the NEW pane
418+ // The focus state machine will apply the active class to the new pane via SetActivePane()
425 if splitTargetContainer != nil {419 if splitTargetContainer != nil {
426 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)420 // Remove active border class if present (prevents GTK bloom filter corruption during unparent)
427 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {421 if webkit.WidgetHasCSSClass(splitTargetContainer, activePaneClass) {
428 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)422 webkit.WidgetRemoveCSSClass(splitTargetContainer, activePaneClass)
423+ log.Printf("[workspace] removed active class from split target (will transfer to new pane)")
429 }424 }
430 }425 }
431 426
432 // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented.427 // Prevent GTK from auto-dropping controllers while the widget is temporarily unparented.
433 wm.safelyDetachControllersBeforeReparent(splitTarget)428 wm.safelyDetachControllersBeforeReparent(splitTarget)
434 429
435- // Detach split target container from its current GTK parent before inserting into new paned.430+ // For non-root splits, unparent from parent paned
436- if parent == nil {431+ if parent != nil && parent.container != nil {
437- // Split target is the root - remove it from the window
438- log.Printf("[workspace] removing split target container=%p from window", splitTargetContainer)
439- if wm.window != nil {
440- wm.window.SetChild(nil)
441- }
442- // GTK4 will auto-unparent when we add to new paned - no manual unparent needed
443- } else if parent.container != nil {
444 // Split target has a parent paned - remove it (automatically unparents in GTK4)432 // Split target has a parent paned - remove it (automatically unparents in GTK4)
445 log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container)433 log.Printf("[workspace] unparenting split target container=%p from parent paned=%p", splitTargetContainer, parent.container)
446 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {434 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {
@@ -453,6 +441,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
453 webkit.WidgetQueueAllocate(parent.container)441 webkit.WidgetQueueAllocate(parent.container)
454 }442 }
455 }443 }
444+ // NOTE: For root splits (parent == nil), we DON'T unparent here.
445+ // We'll unparent immediately before adding to paned below to minimize orphan time.
456 446
457 // Set up the tree structure first447 // Set up the tree structure first
458 if existingFirst {448 if existingFirst {
@@ -479,29 +469,48 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
479 469
480 // GTK4 handles widget operations automatically - no need to force redraw470 // GTK4 handles widget operations automatically - no need to force redraw
481 471
482- // Add both containers to the new paned472+ // Handle root split: attach paned to window FIRST, then let GTK handle reparenting
483- if existingFirst {473+ if parent == nil {
484- if splitTargetContainer != nil {474+ // ROOT SPLIT: Attach empty paned to window, then add children
475+ // GTK will automatically reparent the existing widget when we call SetStartChild/SetEndChild
476+ if wm.window != nil {
477+ // Clear any existing window child (the splitTargetContainer)
478+ wm.window.SetChild(nil)
479+
480+ // Attach the new paned to window FIRST
481+ wm.window.SetChild(paned)
482+ log.Printf("[workspace] ROOT SPLIT: attached paned %p to window", paned)
483+ }
484+
485+ // Now add children to the paned - GTK will handle reparenting automatically
486+ if existingFirst {
485 paned.SetStartChild(splitTargetContainer)487 paned.SetStartChild(splitTargetContainer)
486 paned.SetEndChild(newContainer)488 paned.SetEndChild(newContainer)
487- log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer)489+ log.Printf("[workspace] ROOT SPLIT: added splitTarget=%p (start), new=%p (end)", splitTargetContainer, newContainer)
488- }490+ } else {
489- } else {
490- if splitTargetContainer != nil {
491 paned.SetStartChild(newContainer)491 paned.SetStartChild(newContainer)
492 paned.SetEndChild(splitTargetContainer)492 paned.SetEndChild(splitTargetContainer)
493- log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer)493+ log.Printf("[workspace] ROOT SPLIT: added new=%p (start), splitTarget=%p (end)", newContainer, splitTargetContainer)
494- }
495- }
496-
497- // Attach the new paned to its parent
498- if parent == nil {
499- if wm.window != nil {
500- wm.window.SetChild(paned)
501- paned.QueueAllocate()
502- log.Printf("[workspace] paned set as window child: paned=%p", paned)
503 }494 }
504 } else {495 } else {
496+ // NON-ROOT SPLIT: Standard approach
497+
498+ // Add both containers to the new paned
499+ if existingFirst {
500+ if splitTargetContainer != nil {
501+ paned.SetStartChild(splitTargetContainer)
502+ paned.SetEndChild(newContainer)
503+ log.Printf("[workspace] added splitTarget=%p as start child, new=%p as end child", splitTargetContainer, newContainer)
504+ }
505+ } else {
506+ if splitTargetContainer != nil {
507+ paned.SetStartChild(newContainer)
508+ paned.SetEndChild(splitTargetContainer)
509+ log.Printf("[workspace] added new=%p as start child, splitTarget=%p as end child", newContainer, splitTargetContainer)
510+ }
511+ }
512+
513+ // Attach the new paned to its parent
505 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {514 if parentPaned, ok := parent.container.(*gtk.Paned); ok && parentPaned != nil {
506 if parent.left == split {515 if parent.left == split {
507 parentPaned.SetStartChild(paned)516 parentPaned.SetStartChild(paned)
@@ -519,22 +528,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
519 // Synchronize the divider once the paned has a meaningful allocation.528 // Synchronize the divider once the paned has a meaningful allocation.
520 wm.syncPanedDivider(paned, orientation, split)529 wm.syncPanedDivider(paned, orientation, split)
521 530
522- // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting531+ // GTK4 automatically handles widget rendering after reparenting - no manual updates needed
523- // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues
524- if target != nil && target.container != nil {
525- wm.scheduleIdleGuarded(func() bool {
526- if target == nil || !target.widgetValid || target.container == nil {
527- return false
528- }
529- // Show widget to reconnect WebKit rendering pipeline
530- webkit.WidgetShow(target.container)
531- // Force GTK to recalculate size and recreate rendering surface
532- webkit.WidgetQueueResize(target.container)
533- webkit.WidgetQueueDraw(target.container)
534- log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting")
535- return false
536- }, target)
537- }
538 532
539 wm.viewToNode[newPane.webView] = newLeaf533 wm.viewToNode[newPane.webView] = newLeaf
540 wm.ensureHover(newLeaf)534 wm.ensureHover(newLeaf)
@@ -555,7 +549,7 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
555 // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow549 // For existing popup panes, they're already in app.panes from createPane() in handlePopupReadyToShow
556 log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)")550 log.Printf("[workspace] Skipping app.panes append for existing popup pane (already added)")
557 }551 }
558- 552+
559 if newPane.zoomController != nil {553 if newPane.zoomController != nil {
560 newPane.zoomController.ApplyInitialZoom()554 newPane.zoomController.ApplyInitialZoom()
561 }555 }
@@ -574,6 +568,24 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string, existi
574 if newContainer != nil {568 if newContainer != nil {
575 webkit.WidgetShow(newContainer)569 webkit.WidgetShow(newContainer)
576 }570 }
571+
572+ // CRITICAL: For root splits, also show the split target container after reparenting
573+ // When the split target is reparented from window → paned, GTK doesn't preserve visibility
574+ if parent == nil && splitTargetContainer != nil {
575+ webkit.WidgetShow(splitTargetContainer)
576+ webkit.WidgetQueueAllocate(splitTargetContainer)
577+ webkit.WidgetQueueDraw(splitTargetContainer)
578+ }
579+
580+ // Ensure the underlying WebView widget is visible after reparenting
581+ if parent == nil && splitTarget != nil && splitTarget.pane != nil && splitTarget.pane.webView != nil {
582+ if webViewWidget := splitTarget.pane.webView.AsWidget(); webViewWidget != nil {
583+ webkit.WidgetShow(webViewWidget)
584+ webkit.WidgetQueueAllocate(webViewWidget)
585+ webkit.WidgetQueueDraw(webViewWidget)
586+ }
587+ }
588+
577 // Attach GTK focus controller to new pane589 // Attach GTK focus controller to new pane
578 if wm.focusStateMachine != nil {590 if wm.focusStateMachine != nil {
579 wm.focusStateMachine.attachGTKController(newLeaf)591 wm.focusStateMachine.attachGTKController(newLeaf)
M internal/app/browser/workspace_popup.go +2 -4
@@ -533,10 +533,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
533533
534534 wm.ensureGUIInPane(popupPane)
535535
536- // WebKit handles loading the URL automatically, just ensure visibility
537- if err := wrappedView.Show(); err != nil {
538- log.Printf("[workspace] failed to show popup WebView: %v", err)
539- }
536+ // Widgets are already shown by splitNode - no need to show again
537+ // GTK4 handles visibility automatically during reparenting
540538
541539 log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url)
542540 }
@@ -533,10 +533,8 @@ func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
533 533
534 wm.ensureGUIInPane(popupPane)534 wm.ensureGUIInPane(popupPane)
535 535
536- // WebKit handles loading the URL automatically, just ensure visibility536+ // Widgets are already shown by splitNode - no need to show again
537- if err := wrappedView.Show(); err != nil {537+ // GTK4 handles visibility automatically during reparenting
538- log.Printf("[workspace] failed to show popup WebView: %v", err)
539- }
540 538
541 log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url)539 log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url)
542 }540 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close