Skip to content

Commit ba86504

Browse files
committed
feat: add import summary
1 parent e84c500 commit ba86504

13 files changed

Lines changed: 432 additions & 119 deletions

File tree

docs/design.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,8 @@ The `OneKeymap` app is a cross-platform desktop application that enables users t
77
### Supported Editors
88

99
**VSCode Family:**
10-
- VSCode
11-
- Windsurf
12-
- Windsurf Next
13-
- Cursor
1410

1511
**IntelliJ Family:**
16-
- IntelliJ IDEA (Ultimate & Community)
17-
- PyCharm
18-
- WebStorm
19-
- CLion
20-
- PhpStorm
21-
- RubyMine
22-
- GoLand
23-
- RustRover
2412

2513
**Other Editors:**
2614
- Zed
File renamed without changes.

internal/cmd/import.go

Lines changed: 168 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"errors"
56
"fmt"
67
"io"
78
"log/slog"
89
"os"
910
"path/filepath"
1011
"strings"
12+
"text/template"
1113

1214
tea "github.com/charmbracelet/bubbletea"
1315
"github.com/spf13/cobra"
@@ -145,25 +147,11 @@ func executeImportInteractive(
145147
return err
146148
}
147149

148-
if result != nil && len(result.SkipReport.SkipActions) > 0 {
149-
vm := views.NewImportSkipReportViewModel(result.SkipReport)
150-
p := tea.NewProgram(vm)
151-
if _, err := p.Run(); err != nil {
152-
logger.Error("could not start program", "error", err)
153-
}
150+
if result != nil {
151+
printImportSummary(cmd, result)
154152
}
155153

156-
// Show validation report if there are issues
157-
if result != nil && result.Report != nil &&
158-
(len(result.Report.Issues) > 0 || len(result.Report.Warnings) > 0) {
159-
logger.Info("Validation found issues. Displaying report...")
160-
if err := runValidationReportPreview(result.Report); err != nil {
161-
logger.Warn("Failed to display validation report", "error", err)
162-
}
163-
}
164-
165-
// Show changes preview and get user confirmation
166-
if result != nil && result.Changes != nil {
154+
if result.Changes.HasChanges() {
167155
confirmed, err := runImportChangesPreview(result.Changes)
168156
if err != nil {
169157
logger.Warn("failed to render changes preview", "error", err)
@@ -172,6 +160,8 @@ func executeImportInteractive(
172160
logger.Info("User cancelled applying changes; no file will be written")
173161
return nil
174162
}
163+
} else {
164+
cmd.Println("No changes to import - file will not be updated")
175165
}
176166

177167
return saveImportResult(f.output, result, logger)
@@ -217,6 +207,10 @@ func executeImportNonInteractive(
217207
return err
218208
}
219209

210+
if result != nil {
211+
printImportSummary(cmd, result)
212+
}
213+
220214
return saveImportResult(f.output, result, logger)
221215
}
222216

@@ -278,6 +272,163 @@ func saveImportResult(outputPath string, result *importerapi.ImportResult, logge
278272
return nil
279273
}
280274

275+
type importSkippedView struct {
276+
Action string
277+
KeybindingCount int
278+
Reason string
279+
}
280+
281+
type importSummaryView struct {
282+
TotalImported int
283+
Skipped []importSkippedView
284+
HasValidation bool
285+
ValidationSource string
286+
MappingsProcessed int
287+
MappingsSucceeded int
288+
Issues []string
289+
Warnings []string
290+
}
291+
292+
// nolint: gochecknoglobals
293+
var importSummaryTemplate = template.Must(template.New("importSummary").Parse(`
294+
Import Summary:
295+
✓ {{ .TotalImported }} actions imported into onekeymap
296+
{{- $skippedCount := len .Skipped }}
297+
{{- if eq $skippedCount 0 }}
298+
✗ 0 editor actions skipped
299+
{{- else }}
300+
✗ {{ $skippedCount }} editor actions skipped:
301+
{{- range .Skipped }}
302+
- {{ .Action }}{{ if gt .KeybindingCount 0 }} ({{ .KeybindingCount }} keybindings){{ end }}{{ if .Reason }}: {{ .Reason }}{{ end }}
303+
{{- end }}
304+
{{- end }}
305+
{{- if .HasValidation }}
306+
307+
Validation Summary:
308+
Source: {{ .ValidationSource }} | Mappings Processed: {{ .MappingsProcessed }} | Succeeded: {{ .MappingsSucceeded }}
309+
Issues: {{ len .Issues }}, Warnings: {{ len .Warnings }}
310+
{{- if gt (len .Issues) 0 }}
311+
Issues ({{ len .Issues }}):
312+
{{- range .Issues }}
313+
- {{ . }}
314+
{{- end }}
315+
{{- end }}
316+
{{- if gt (len .Warnings) 0 }}
317+
Warnings ({{ len .Warnings }}):
318+
{{- range .Warnings }}
319+
- {{ . }}
320+
{{- end }}
321+
{{- end }}
322+
{{- end }}
323+
`))
324+
325+
func printImportSummary(cmd *cobra.Command, result *importerapi.ImportResult) {
326+
view := importSummaryView{
327+
TotalImported: len(result.Setting.Actions),
328+
}
329+
330+
for _, sk := range result.SkipReport.SkipActions {
331+
item := importSkippedView{
332+
Action: sk.EditorSpecificAction,
333+
KeybindingCount: len(sk.Keybindings),
334+
}
335+
if sk.Error != nil {
336+
item.Reason = sk.Error.Error()
337+
}
338+
view.Skipped = append(view.Skipped, item)
339+
}
340+
341+
if result.Report != nil {
342+
rep := result.Report
343+
view.HasValidation = true
344+
view.ValidationSource = rep.SourceEditor
345+
view.MappingsProcessed = rep.Summary.MappingsProcessed
346+
view.MappingsSucceeded = rep.Summary.MappingsSucceeded
347+
for _, issue := range rep.Issues {
348+
view.Issues = append(view.Issues, renderValidationIssueInline(issue))
349+
}
350+
for _, warning := range rep.Warnings {
351+
view.Warnings = append(view.Warnings, renderValidationIssueInline(warning))
352+
}
353+
}
354+
355+
var buf bytes.Buffer
356+
if err := importSummaryTemplate.Execute(&buf, view); err != nil {
357+
// Fallback to minimal output if template rendering fails
358+
cmd.Println()
359+
cmd.Println("Import Summary:")
360+
cmd.Printf(" ✓ %d actions imported into onekeymap\n", view.TotalImported)
361+
return
362+
}
363+
364+
cmd.Println()
365+
cmd.Print(buf.String())
366+
}
367+
368+
// renderValidationIssueInline renders a single validation issue in a compact textual form,
369+
// mirroring the semantics of views.renderIssue but without TUI styling.
370+
func renderValidationIssueInline(issue validateapi.ValidationIssue) string {
371+
switch issue.Type {
372+
case validateapi.IssueTypeKeybindConflict:
373+
if c, ok := issue.Details.(validateapi.KeybindConflict); ok {
374+
var actionLines []string
375+
for _, action := range c.Actions {
376+
if action.Context != "" {
377+
actionLines = append(actionLines, fmt.Sprintf("%s (%s)", action.Context, action.ActionID))
378+
} else {
379+
actionLines = append(actionLines, action.ActionID)
380+
}
381+
}
382+
return fmt.Sprintf(
383+
"Keybind Conflict: %s is mapped to multiple actions:\n - %s",
384+
c.Keybinding,
385+
strings.Join(actionLines, "\n - "),
386+
)
387+
}
388+
case validateapi.IssueTypeDanglingAction:
389+
if d, ok := issue.Details.(validateapi.DanglingAction); ok {
390+
suggestion := ""
391+
if d.Suggestion != "" {
392+
suggestion = fmt.Sprintf(" (%s)", d.Suggestion)
393+
}
394+
return fmt.Sprintf(
395+
"Dangling Action: %s does not exist in target %s.%s",
396+
d.Action,
397+
d.TargetEditor,
398+
suggestion,
399+
)
400+
}
401+
case validateapi.IssueTypeUnsupportedAction:
402+
if u, ok := issue.Details.(validateapi.UnsupportedAction); ok {
403+
return fmt.Sprintf(
404+
"Unsupported Action: %s (on key %s) is not supported for target %s.",
405+
u.Action,
406+
u.Keybinding,
407+
u.TargetEditor,
408+
)
409+
}
410+
case validateapi.IssueTypeDuplicateMapping:
411+
if d, ok := issue.Details.(validateapi.DuplicateMapping); ok {
412+
return fmt.Sprintf(
413+
"Duplicate Mapping: Action %s with key %s is defined multiple times.",
414+
d.Action,
415+
d.Keybinding,
416+
)
417+
}
418+
case validateapi.IssueTypePotentialShadowing:
419+
if p, ok := issue.Details.(validateapi.PotentialShadowing); ok {
420+
return fmt.Sprintf(
421+
"Potential Shadowing: Key %s (for action %s). %s",
422+
p.Keybinding,
423+
p.Action,
424+
p.CriticalShortcutDescription,
425+
)
426+
}
427+
}
428+
429+
return "Unknown issue type."
430+
}
431+
281432
func handleInteractiveImportFlags(
282433
cmd *cobra.Command,
283434
f *importFlags,
@@ -415,13 +566,3 @@ func runImportForm(
415566
}
416567
return nil
417568
}
418-
419-
// run the validation report TUI ---.
420-
func runValidationReportPreview(report *validateapi.ValidationReport) error {
421-
m := views.NewValidationReportModel(report)
422-
p := tea.NewProgram(m)
423-
if _, err := p.Run(); err != nil {
424-
return err
425-
}
426-
return nil
427-
}

internal/imports/marker.go

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,63 @@
11
package imports
22

33
import (
4+
"github.com/xinnjie/onekeymap-cli/pkg/api/keymap/keybinding"
45
"github.com/xinnjie/onekeymap-cli/pkg/api/pluginapi"
56
)
67

78
type Marker struct {
89
imported map[string]bool
9-
skipped map[string]error
10+
skipped map[string]skippedEntry
1011
order []string
12+
13+
// importedResults tracks detailed import results keyed by "mappedAction:editorAction"
14+
importedResults map[string]*pluginapi.KeybindingImportResult
15+
importedOrder []string
16+
}
17+
18+
type skippedEntry struct {
19+
err error
20+
keybindings []keybinding.Keybinding
1121
}
1222

1323
func NewMarker() *Marker {
1424
return &Marker{
15-
imported: make(map[string]bool),
16-
skipped: make(map[string]error),
25+
imported: make(map[string]bool),
26+
skipped: make(map[string]skippedEntry),
27+
importedResults: make(map[string]*pluginapi.KeybindingImportResult),
1728
}
1829
}
1930

20-
func (m *Marker) MarkImported(editorSpecificAction string) {
21-
if editorSpecificAction == "" {
31+
// MarkImported records a successful import with full details for coverage reporting.
32+
func (m *Marker) MarkImported(
33+
mappedAction string,
34+
editorSpecificAction string,
35+
originalKeybinding keybinding.Keybinding,
36+
importedKeybinding keybinding.Keybinding,
37+
) {
38+
if mappedAction == "" || editorSpecificAction == "" {
2239
return
2340
}
41+
// mark as imported and clear any previous skipped entry
2442
m.imported[editorSpecificAction] = true
2543
delete(m.skipped, editorSpecificAction)
44+
45+
key := mappedAction + "\x00" + editorSpecificAction
46+
result, exists := m.importedResults[key]
47+
if !exists {
48+
result = &pluginapi.KeybindingImportResult{
49+
MappedAction: mappedAction,
50+
EditorSpecificAction: editorSpecificAction,
51+
}
52+
m.importedResults[key] = result
53+
m.importedOrder = append(m.importedOrder, key)
54+
}
55+
result.OriginalKeybindings = append(result.OriginalKeybindings, originalKeybinding)
56+
result.ImportedKeybindings = append(result.ImportedKeybindings, importedKeybinding)
2657
}
2758

28-
func (m *Marker) MarkSkippedForReason(editorSpecificAction string, reasonErr error) {
59+
// MarkSkipped records a skipped action with its keybinding for coverage reporting.
60+
func (m *Marker) MarkSkipped(editorSpecificAction string, kb *keybinding.Keybinding, reasonErr error) {
2961
if editorSpecificAction == "" {
3062
return
3163
}
@@ -35,10 +67,15 @@ func (m *Marker) MarkSkippedForReason(editorSpecificAction string, reasonErr err
3567
if m.imported[editorSpecificAction] {
3668
return
3769
}
38-
if _, exists := m.skipped[editorSpecificAction]; !exists {
39-
m.skipped[editorSpecificAction] = reasonErr
70+
entry, exists := m.skipped[editorSpecificAction]
71+
if !exists {
72+
entry = skippedEntry{err: reasonErr}
4073
m.order = append(m.order, editorSpecificAction)
4174
}
75+
if kb != nil {
76+
entry.keybindings = append(entry.keybindings, *kb)
77+
}
78+
m.skipped[editorSpecificAction] = entry
4279
}
4380

4481
func (m *Marker) Report() pluginapi.ImportSkipReport {
@@ -47,12 +84,24 @@ func (m *Marker) Report() pluginapi.ImportSkipReport {
4784
if m.imported[action] {
4885
continue
4986
}
50-
if err, ok := m.skipped[action]; ok {
87+
if entry, ok := m.skipped[action]; ok {
5188
result = append(result, pluginapi.ImportSkipAction{
5289
EditorSpecificAction: action,
53-
Error: err,
90+
Keybindings: entry.keybindings,
91+
Error: entry.err,
5492
})
5593
}
5694
}
5795
return pluginapi.ImportSkipReport{SkipActions: result}
5896
}
97+
98+
// ImportedReport returns the detailed import results for coverage reporting.
99+
func (m *Marker) ImportedReport() pluginapi.ImportedReport {
100+
results := make([]pluginapi.KeybindingImportResult, 0, len(m.importedResults))
101+
for _, key := range m.importedOrder {
102+
if result, ok := m.importedResults[key]; ok {
103+
results = append(results, *result)
104+
}
105+
}
106+
return pluginapi.ImportedReport{Results: results}
107+
}

0 commit comments

Comments
 (0)