Skip to content

Commit 60f2a9f

Browse files
committed
feat: unprintable key code support in xcode plugin
1 parent c39189c commit 60f2a9f

10 files changed

Lines changed: 1412 additions & 87 deletions

File tree

internal/mappings/mapping_config_xcode.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
type XcodeMappingConfig struct {
1212
EditorActionMapping `yaml:",inline"`
1313

14+
// The Xcode text action name for Text Key Bindings (e.g., "pageDown:", "deleteBackward:")
15+
TextAction string `yaml:"textAction,omitempty"`
16+
1417
// The Xcode action name (e.g., "moveWordLeft:", "selectWord:")
1518
Action string `yaml:"action"`
1619
// The command group ID
@@ -65,30 +68,46 @@ func (x *XcodeConfigs) UnmarshalYAML(node *yaml.Node) error {
6568
}
6669

6770
func checkXcodeDuplicateConfig(mappings map[string]ActionMappingConfig) error {
68-
seen := make(map[struct{ Action, CommandID string }]string)
71+
seenMenuBindings := make(map[struct{ Action, CommandID string }]string)
72+
seenTextBindings := make(map[string]string)
6973
dups := make(map[string][]string) // key string -> list of universal action IDs
74+
7075
for id, mapping := range mappings {
7176
for _, xcodeConfig := range mapping.Xcode {
72-
if xcodeConfig.Action == "" {
73-
continue
74-
}
7577
// Skip configs that are disabled for import (export-only)
7678
if xcodeConfig.DisableImport {
7779
continue
7880
}
7981

80-
key := struct{ Action, CommandID string }{xcodeConfig.Action, xcodeConfig.CommandID}
81-
if originalID, exists := seen[key]; exists {
82-
dupKey := fmt.Sprintf(`{"action":%q,"commandID":%q}`, key.Action, key.CommandID)
83-
if _, ok := dups[dupKey]; !ok {
84-
dups[dupKey] = []string{originalID}
82+
// Check Menu Key Bindings (Action + CommandID)
83+
if xcodeConfig.Action != "" {
84+
key := struct{ Action, CommandID string }{xcodeConfig.Action, xcodeConfig.CommandID}
85+
if originalID, exists := seenMenuBindings[key]; exists {
86+
dupKey := fmt.Sprintf(`{"action":%q,"commandID":%q}`, key.Action, key.CommandID)
87+
if _, ok := dups[dupKey]; !ok {
88+
dups[dupKey] = []string{originalID}
89+
}
90+
dups[dupKey] = append(dups[dupKey], id)
91+
} else {
92+
seenMenuBindings[key] = id
93+
}
94+
}
95+
96+
// Check Text Key Bindings (TextAction)
97+
if xcodeConfig.TextAction != "" {
98+
if originalID, exists := seenTextBindings[xcodeConfig.TextAction]; exists {
99+
dupKey := fmt.Sprintf(`{"textAction":%q}`, xcodeConfig.TextAction)
100+
if _, ok := dups[dupKey]; !ok {
101+
dups[dupKey] = []string{originalID}
102+
}
103+
dups[dupKey] = append(dups[dupKey], id)
104+
} else {
105+
seenTextBindings[xcodeConfig.TextAction] = id
85106
}
86-
dups[dupKey] = append(dups[dupKey], id)
87-
continue
88107
}
89-
seen[key] = id
90108
}
91109
}
110+
92111
if len(dups) == 0 {
93112
return nil
94113
}

internal/plugins/xcode/export.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func (e *xcodeExporter) Export(
8383
) (*pluginapi.PluginExportReport, error) {
8484
// Decode existing config for non-destructive merge
8585
var existingKeybindings []xcodeKeybinding
86+
var existingTextKeybindings xcodeTextKeybinding
8687
if opts.ExistingConfig != nil {
8788
// Read all content first
8889
rawData, err := io.ReadAll(opts.ExistingConfig)
@@ -95,6 +96,7 @@ func (e *xcodeExporter) Export(
9596
return nil, fmt.Errorf("failed to decode existing config: %w", err)
9697
}
9798
existingKeybindings = plistData.MenuKeyBindings.KeyBindings
99+
existingTextKeybindings = plistData.TextKeyBindings.KeyBindings
98100
}
99101

100102
var unmanagedKeybindings []xcodeKeybinding
@@ -109,14 +111,22 @@ func (e *xcodeExporter) Export(
109111
// Reorder according to base command order if provided
110112
finalKeybindings = orderByBaseCommand(finalKeybindings, existingKeybindings)
111113

114+
// Generate text key bindings
115+
finalTextKeybindings := e.generateTextKeyBindings(setting, existingTextKeybindings)
116+
112117
// Use plist library to generate the XML
113118
plistData := xcodeKeybindingsPlist{
114119
MenuKeyBindings: menuKeyBindings{
115120
KeyBindings: finalKeybindings,
116121
},
122+
TextKeyBindings: textKeyBindings{
123+
KeyBindings: finalTextKeybindings,
124+
},
117125
}
118126

119-
if err := plist.NewEncoder(destination).Encode(plistData); err != nil {
127+
encoder := plist.NewEncoder(destination)
128+
encoder.Indent("\t")
129+
if err := encoder.Encode(plistData); err != nil {
120130
return nil, fmt.Errorf("failed to write plist XML: %w", err)
121131
}
122132

@@ -146,6 +156,7 @@ func (e *xcodeExporter) identifyUnmanagedKeybindings(existingKeybindings []xcode
146156

147157
// findMappingByXcodeKeybinding performs reverse lookup to find if an Xcode keybinding
148158
// corresponds to any action in our mappings.
159+
// It checks both Action (for Menu Key Bindings) and CommandID.
149160
func (e *xcodeExporter) findMappingByXcodeKeybinding(kb xcodeKeybinding) *mappings.ActionMappingConfig {
150161
for _, mapping := range e.mappingConfig.Mappings {
151162
for _, xcodeConfig := range mapping.Xcode {
@@ -230,3 +241,51 @@ func (e *xcodeExporter) mergeKeybindings(managed, unmanaged []xcodeKeybinding) [
230241

231242
return result
232243
}
244+
245+
// generateTextKeyBindings generates Xcode Text Key Bindings from KeymapSetting.
246+
// It merges managed text bindings with existing ones, with managed taking priority.
247+
func (e *xcodeExporter) generateTextKeyBindings(
248+
setting *keymapv1.Keymap,
249+
existingTextBindings xcodeTextKeybinding,
250+
) xcodeTextKeybinding {
251+
// Start with existing text bindings or create new map
252+
result := make(xcodeTextKeybinding)
253+
for k, v := range existingTextBindings {
254+
result[k] = v
255+
}
256+
257+
// Generate managed text bindings
258+
for _, km := range setting.GetActions() {
259+
mapping := e.mappingConfig.Get(km.GetName())
260+
if mapping == nil {
261+
continue
262+
}
263+
264+
xcodeConfigs := mapping.Xcode
265+
if len(xcodeConfigs) == 0 {
266+
continue
267+
}
268+
269+
for _, b := range km.GetBindings() {
270+
if b == nil {
271+
continue
272+
}
273+
binding := keymap.NewKeyBinding(b)
274+
keys, err := formatKeybinding(binding)
275+
if err != nil {
276+
e.logger.Warn("Skipping text keybinding with un-formattable key", "action", km.GetName(), "error", err)
277+
continue
278+
}
279+
280+
for _, xcodeConfig := range xcodeConfigs {
281+
if xcodeConfig.TextAction == "" {
282+
continue
283+
}
284+
// Managed text bindings override existing ones with same key
285+
result[keys] = xcodeConfig.TextAction
286+
}
287+
}
288+
}
289+
290+
return result
291+
}

0 commit comments

Comments
 (0)