v0.33.1 9577fee

vim_navigation.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"errors"
6	"fmt"
7
8	"github.com/bnema/dumber/internal/application/dto"
9	"github.com/bnema/dumber/internal/application/port"
10)
11
12var (
13	errUnsupportedVimNavigationAction = errors.New("vim navigation: unsupported action")
14	errUnsupportedVimNavigationEngine = errors.New("vim navigation: unsupported webview capability")
15)
16
17// VimNavigationUseCase maps configured Vim Mode navigation actions to optional
18// WebView capabilities. The UI layer supplies the active WebView and remains
19// responsible only for event and theme context.
20type VimNavigationUseCase struct{}
21
22// NewVimNavigationUseCase creates a stateless Vim navigation use case.
23func NewVimNavigationUseCase() *VimNavigationUseCase {
24	return &VimNavigationUseCase{}
25}
26
27// Execute dispatches a configured Vim action to the active WebView. The
28// outcome tells the UI whether the page now owns key input.
29func (uc *VimNavigationUseCase) Execute(
30	ctx context.Context, wv port.WebView, action string, count int, highlightColor string,
31) (dto.VimNavigationOutcome, error) {
32	if wv == nil {
33		return dto.VimNavigationOutcome{}, errors.New("vim navigation: nil webview")
34	}
35	if request, ok := vimPageInteractionRequest(action, highlightColor); ok {
36		return uc.startPageInteraction(ctx, wv, request)
37	}
38	return dto.VimNavigationOutcome{}, executeVimNavigation(ctx, wv, action, count, highlightColor)
39}
40
41func executeVimNavigation(ctx context.Context, wv port.WebView, action string, count int, highlightColor string) error {
42	switch action {
43	case "confirm":
44		activator, ok := wv.(port.SemanticNavigationActivator)
45		if !ok {
46			return errUnsupportedVimNavigationEngine
47		}
48		return activator.ActivateSemanticNavigationTarget(ctx)
49	case "focus-input":
50		focuser, ok := wv.(port.PageInputFocuser)
51		if !ok {
52			return errUnsupportedVimNavigationEngine
53		}
54		focuser.FocusNextInput()
55		return nil
56	}
57
58	var request dto.SemanticNavigationRequest
59	switch action {
60	case "heading-next":
61		request = dto.SemanticNavigationRequest{
62			Target:         dto.SemanticNavigationTargetHeading,
63			Direction:      dto.SemanticNavigationForward,
64			Count:          count,
65			HighlightColor: highlightColor,
66		}
67	case "heading-prev":
68		request = dto.SemanticNavigationRequest{
69			Target:         dto.SemanticNavigationTargetHeading,
70			Direction:      dto.SemanticNavigationBackward,
71			Count:          count,
72			HighlightColor: highlightColor,
73		}
74	default:
75		return errUnsupportedVimNavigationAction
76	}
77
78	navigator, ok := wv.(port.SemanticNavigable)
79	if !ok {
80		return errUnsupportedVimNavigationEngine
81	}
82	return navigator.NavigateSemantic(ctx, request)
83}
84
85var vimYankObjects = map[string]dto.VimPageTextObject{
86	"yank-section":   dto.VimPageTextObjectSection,
87	"yank-paragraph": dto.VimPageTextObjectParagraph,
88	"yank-code":      dto.VimPageTextObjectCode,
89	"yank-table":     dto.VimPageTextObjectTable,
90	"yank-list":      dto.VimPageTextObjectList,
91}
92
93func vimPageInteractionRequest(action, highlightColor string) (dto.VimPageInteractionRequest, bool) {
94	request := dto.VimPageInteractionRequest{HighlightColor: highlightColor}
95	switch action {
96	case "hint-follow":
97		request.Kind = dto.VimPageHintFollow
98	case "hint-follow-new":
99		request.Kind = dto.VimPageHintFollowNew
100	case "hint-yank-url":
101		request.Kind = dto.VimPageHintYankURL
102	case "visual":
103		request.Kind = dto.VimPageVisual
104	default:
105		object, ok := vimYankObjects[action]
106		if !ok {
107			return dto.VimPageInteractionRequest{}, false
108		}
109		request.Kind = dto.VimPageYankObject
110		request.Object = object
111	}
112	return request, true
113}
114
115func (*VimNavigationUseCase) startPageInteraction(
116	ctx context.Context, wv port.WebView, request dto.VimPageInteractionRequest,
117) (dto.VimNavigationOutcome, error) {
118	interactor, ok := wv.(port.VimPageInteractor)
119	if !ok {
120		return dto.VimNavigationOutcome{}, errUnsupportedVimNavigationEngine
121	}
122	if err := interactor.StartVimPageInteraction(ctx, request); err != nil {
123		return dto.VimNavigationOutcome{}, fmt.Errorf("vim navigation: start page interaction: %w", err)
124	}
125	if !request.Kind.CapturesKeys() {
126		return dto.VimNavigationOutcome{}, nil
127	}
128	// Every key-capturing interaction opens on text or link hints; the page
129	// reports later sub-modes itself.
130	return dto.VimNavigationOutcome{CapturePageKeys: true, PageKind: request.Kind, PageMode: dto.VimPageModeHints}, nil
131}
132
133// SendPageKey forwards one canonical key to the active in-page interaction.
134func (*VimNavigationUseCase) SendPageKey(ctx context.Context, wv port.WebView, key string) error {
135	interactor, ok := wv.(port.VimPageInteractor)
136	if !ok {
137		return errUnsupportedVimNavigationEngine
138	}
139	if err := interactor.SendVimPageKey(ctx, key); err != nil {
140		return fmt.Errorf("vim navigation: send page key: %w", err)
141	}
142	return nil
143}
144
145// CancelPageInteraction ends any in-page interaction, removing its overlay or
146// selection. WebViews without the capability have nothing to cancel.
147func (*VimNavigationUseCase) CancelPageInteraction(ctx context.Context, wv port.WebView) error {
148	interactor, ok := wv.(port.VimPageInteractor)
149	if !ok {
150		return nil
151	}
152	if err := interactor.CancelVimPageInteraction(ctx); err != nil {
153		return fmt.Errorf("vim navigation: cancel page interaction: %w", err)
154	}
155	return nil
156}
157
158// ClearSemanticNavigationHighlight removes the visual target left by semantic
159// navigation when the active Vim Mode session ends.
160func (*VimNavigationUseCase) ClearSemanticNavigationHighlight(ctx context.Context, wv port.WebView) error {
161	if wv == nil {
162		return nil
163	}
164	clearer, ok := wv.(port.SemanticNavigationHighlightClearer)
165	if !ok {
166		return nil
167	}
168	return clearer.ClearSemanticNavigationHighlight(ctx)
169}
170
171// NavigatePageFocus keeps focus traversal inside a page when the active page
172// control would otherwise hand the host window a Tab event.
173func (*VimNavigationUseCase) NavigatePageFocus(wv port.WebView, backward bool) bool {
174	if wv == nil {
175		return false
176	}
177	navigator, ok := wv.(port.PageFocusNavigator)
178	if !ok {
179		return false
180	}
181	navigator.NavigatePageFocus(backward)
182	return true
183}

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