@@ -17,6 +17,11 @@ import {
1717 ResolvedKeybindingsConfig ,
1818 type ServerConfigIssue ,
1919} from "@okcode/contracts" ;
20+ import {
21+ DEFAULT_KEYBINDINGS as SHARED_DEFAULT_KEYBINDINGS ,
22+ encodeKeybindingShortcut ,
23+ parseKeybindingShortcut as parseSharedKeybindingShortcut ,
24+ } from "@okcode/shared/keybindings" ;
2025import { Mutable } from "effect/Types" ;
2126import {
2227 Array ,
@@ -64,90 +69,10 @@ type WhenToken =
6469 | { type : "lparen" }
6570 | { type : "rparen" } ;
6671
67- export const DEFAULT_KEYBINDINGS : ReadonlyArray < KeybindingRule > = [
68- { key : "mod+j" , command : "terminal.toggle" } ,
69- { key : "ctrl+`" , command : "terminal.toggle" } ,
70- { key : "mod+d" , command : "terminal.split" , when : "terminalFocus" } ,
71- { key : "mod+n" , command : "terminal.new" , when : "terminalFocus" } ,
72- { key : "mod+w" , command : "terminal.close" , when : "terminalFocus" } ,
73- { key : "mod+n" , command : "chat.new" , when : "!terminalFocus" } ,
74- { key : "mod+shift+o" , command : "chat.new" , when : "!terminalFocus" } ,
75- { key : "mod+shift+n" , command : "chat.newLocal" , when : "!terminalFocus" } ,
76- { key : "mod+down" , command : "git.pullRequest" , when : "!terminalFocus" } ,
77- { key : "mod+shift+p" , command : "git.pullRequest" , when : "!terminalFocus" } ,
78- { key : "mod+o" , command : "editor.openFavorite" } ,
79- ] ;
80-
81- function normalizeKeyToken ( token : string ) : string {
82- if ( token === "space" ) return " " ;
83- if ( token === "esc" ) return "escape" ;
84- return token ;
85- }
72+ export const DEFAULT_KEYBINDINGS = SHARED_DEFAULT_KEYBINDINGS ;
8673
8774/** @internal - Exported for testing */
88- export function parseKeybindingShortcut ( value : string ) : KeybindingShortcut | null {
89- const rawTokens = value
90- . toLowerCase ( )
91- . split ( "+" )
92- . map ( ( token ) => token . trim ( ) ) ;
93- const tokens = [ ...rawTokens ] ;
94- let trailingEmptyCount = 0 ;
95- while ( tokens [ tokens . length - 1 ] === "" ) {
96- trailingEmptyCount += 1 ;
97- tokens . pop ( ) ;
98- }
99- if ( trailingEmptyCount > 0 ) {
100- tokens . push ( "+" ) ;
101- }
102- if ( tokens . some ( ( token ) => token . length === 0 ) ) {
103- return null ;
104- }
105- if ( tokens . length === 0 ) return null ;
106-
107- let key : string | null = null ;
108- let metaKey = false ;
109- let ctrlKey = false ;
110- let shiftKey = false ;
111- let altKey = false ;
112- let modKey = false ;
113-
114- for ( const token of tokens ) {
115- switch ( token ) {
116- case "cmd" :
117- case "meta" :
118- metaKey = true ;
119- break ;
120- case "ctrl" :
121- case "control" :
122- ctrlKey = true ;
123- break ;
124- case "shift" :
125- shiftKey = true ;
126- break ;
127- case "alt" :
128- case "option" :
129- altKey = true ;
130- break ;
131- case "mod" :
132- modKey = true ;
133- break ;
134- default : {
135- if ( key !== null ) return null ;
136- key = normalizeKeyToken ( token ) ;
137- }
138- }
139- }
140-
141- if ( key === null ) return null ;
142- return {
143- key,
144- metaKey,
145- ctrlKey,
146- shiftKey,
147- altKey,
148- modKey,
149- } ;
150- }
75+ export const parseKeybindingShortcut = parseSharedKeybindingShortcut ;
15176
15277function tokenizeWhenExpression ( expression : string ) : WhenToken [ ] | null {
15378 const tokens : WhenToken [ ] = [ ] ;
@@ -383,16 +308,7 @@ function hasSameShortcutContext(left: KeybindingRule, right: KeybindingRule): bo
383308}
384309
385310function encodeShortcut ( shortcut : KeybindingShortcut ) : string | null {
386- const modifiers : string [ ] = [ ] ;
387- if ( shortcut . modKey ) modifiers . push ( "mod" ) ;
388- if ( shortcut . metaKey ) modifiers . push ( "meta" ) ;
389- if ( shortcut . ctrlKey ) modifiers . push ( "ctrl" ) ;
390- if ( shortcut . altKey ) modifiers . push ( "alt" ) ;
391- if ( shortcut . shiftKey ) modifiers . push ( "shift" ) ;
392- if ( ! shortcut . key ) return null ;
393- if ( shortcut . key !== "+" && shortcut . key . includes ( "+" ) ) return null ;
394- const key = shortcut . key === " " ? "space" : shortcut . key ;
395- return [ ...modifiers , key ] . join ( "+" ) ;
311+ return encodeKeybindingShortcut ( shortcut ) ;
396312}
397313
398314function encodeWhenAst ( node : KeybindingWhenNode ) : string {
@@ -521,6 +437,17 @@ export interface KeybindingsShape {
521437 readonly upsertKeybindingRule : (
522438 rule : KeybindingRule ,
523439 ) => Effect . Effect < ResolvedKeybindingsConfig , KeybindingsConfigError > ;
440+
441+ /**
442+ * Replace every persisted rule for a command with the provided rules.
443+ *
444+ * Passing an empty array removes custom rules for the command so defaults
445+ * can flow through again for built-in commands.
446+ */
447+ readonly replaceKeybindingRules : (
448+ command : KeybindingRule [ "command" ] ,
449+ rules : readonly KeybindingRule [ ] ,
450+ ) => Effect . Effect < ResolvedKeybindingsConfig , KeybindingsConfigError > ;
524451}
525452
526453/**
@@ -854,6 +781,46 @@ const makeKeybindings = Effect.gen(function* () {
854781 yield * Deferred . succeed ( startedDeferred , undefined ) . pipe ( Effect . orDie ) ;
855782 } ) ;
856783
784+ const replaceKeybindingRules = (
785+ command : KeybindingRule [ "command" ] ,
786+ rules : readonly KeybindingRule [ ] ,
787+ ) =>
788+ upsertSemaphore . withPermits ( 1 ) (
789+ Effect . gen ( function * ( ) {
790+ if ( rules . some ( ( rule ) => rule . command !== command ) ) {
791+ return yield * new KeybindingsConfigError ( {
792+ configPath : keybindingsConfigPath ,
793+ detail : `received mismatched command rules for ${ command } ` ,
794+ } ) ;
795+ }
796+ const customConfig = yield * loadWritableCustomKeybindingsConfig ( ) ;
797+ const nextConfig = [ ...customConfig . filter ( ( entry ) => entry . command !== command ) , ...rules ] ;
798+ const cappedConfig =
799+ nextConfig . length > MAX_KEYBINDINGS_COUNT
800+ ? nextConfig . slice ( - MAX_KEYBINDINGS_COUNT )
801+ : nextConfig ;
802+ if ( nextConfig . length > MAX_KEYBINDINGS_COUNT ) {
803+ yield * Effect . logWarning ( "truncating keybindings config to max entries" , {
804+ path : keybindingsConfigPath ,
805+ maxEntries : MAX_KEYBINDINGS_COUNT ,
806+ } ) ;
807+ }
808+ yield * writeConfigAtomically ( cappedConfig ) ;
809+ const nextResolved = mergeWithDefaultKeybindings (
810+ compileResolvedKeybindingsConfig ( cappedConfig ) ,
811+ ) ;
812+ yield * Cache . set ( resolvedConfigCache , resolvedConfigCacheKey , {
813+ keybindings : nextResolved ,
814+ issues : [ ] ,
815+ } ) ;
816+ yield * emitChange ( {
817+ keybindings : nextResolved ,
818+ issues : [ ] ,
819+ } ) ;
820+ return nextResolved ;
821+ } ) ,
822+ ) ;
823+
857824 return {
858825 start,
859826 ready : Deferred . await ( startedDeferred ) ,
@@ -863,39 +830,8 @@ const makeKeybindings = Effect.gen(function* () {
863830 get streamChanges ( ) {
864831 return Stream . fromPubSub ( changesPubSub ) ;
865832 } ,
866- upsertKeybindingRule : ( rule ) =>
867- upsertSemaphore . withPermits ( 1 ) (
868- Effect . gen ( function * ( ) {
869- const customConfig = yield * loadWritableCustomKeybindingsConfig ( ) ;
870- const nextConfig = [
871- ...customConfig . filter ( ( entry ) => entry . command !== rule . command ) ,
872- rule ,
873- ] ;
874- const cappedConfig =
875- nextConfig . length > MAX_KEYBINDINGS_COUNT
876- ? nextConfig . slice ( - MAX_KEYBINDINGS_COUNT )
877- : nextConfig ;
878- if ( nextConfig . length > MAX_KEYBINDINGS_COUNT ) {
879- yield * Effect . logWarning ( "truncating keybindings config to max entries" , {
880- path : keybindingsConfigPath ,
881- maxEntries : MAX_KEYBINDINGS_COUNT ,
882- } ) ;
883- }
884- yield * writeConfigAtomically ( cappedConfig ) ;
885- const nextResolved = mergeWithDefaultKeybindings (
886- compileResolvedKeybindingsConfig ( cappedConfig ) ,
887- ) ;
888- yield * Cache . set ( resolvedConfigCache , resolvedConfigCacheKey , {
889- keybindings : nextResolved ,
890- issues : [ ] ,
891- } ) ;
892- yield * emitChange ( {
893- keybindings : nextResolved ,
894- issues : [ ] ,
895- } ) ;
896- return nextResolved ;
897- } ) ,
898- ) ,
833+ replaceKeybindingRules,
834+ upsertKeybindingRule : ( rule ) => replaceKeybindingRules ( rule . command , [ rule ] ) ,
899835 } satisfies KeybindingsShape ;
900836} ) ;
901837
0 commit comments