@@ -23,6 +23,24 @@ class TerminalSetupCommand(BaseCommand):
2323 }
2424
2525 WT_KEYBINDING = {"id" : "User.sendInput.shift_enter" , "keys" : "shift+enter" }
26+ # VS Code configuration constants
27+ VSCODE_SHIFT_ENTER_SEQUENCE = "\n "
28+
29+ VSCODE_SHIFT_ENTER_BINDING = {
30+ "key" : "shift+enter" ,
31+ "command" : "workbench.action.terminal.sendSequence" ,
32+ "when" : "terminalFocus" ,
33+ "args" : {"text" : "\n " },
34+ }
35+
36+ @staticmethod
37+ def _strip_json_comments (content : str ) -> str :
38+ """Remove single-line JSON comments (// ...) from a string to allow parsing
39+ VS Code style JSON files that may contain comments."""
40+ # Remove single-line comments (// ...)
41+ import re
42+
43+ return re .sub (r"^\s*//.*$" , "" , content , flags = re .MULTILINE )
2644
2745 @classmethod
2846 def _get_config_paths (cls ):
@@ -38,6 +56,7 @@ def _get_config_paths(cls):
3856 # Standard Linux paths (applies to WSL instances of Kitty/Alacritty too)
3957 paths ["alacritty" ] = home / ".config" / "alacritty" / "alacritty.toml"
4058 paths ["kitty" ] = home / ".config" / "kitty" / "kitty.conf"
59+ paths ["vscode" ] = home / ".config" / "Code" / "User" / "keybindings.json"
4160
4261 if is_wsl_env :
4362 # Try to find Windows Terminal settings from inside WSL
@@ -62,11 +81,17 @@ def _get_config_paths(cls):
6281 elif system == "Darwin" : # macOS
6382 paths ["alacritty" ] = home / ".config" / "alacritty" / "alacritty.toml"
6483 paths ["kitty" ] = home / ".config" / "kitty" / "kitty.conf"
84+ # VS Code on macOS
85+ paths ["vscode" ] = (
86+ home / "Library" / "Application Support" / "Code" / "User" / "keybindings.json"
87+ )
6588
6689 elif system == "Windows" :
6790 appdata = Path (os .getenv ("APPDATA" ))
6891 paths ["alacritty" ] = appdata / "alacritty" / "alacritty.toml"
6992 paths ["kitty" ] = appdata / "kitty" / "kitty.conf"
93+ # VS Code on Windows
94+ paths ["vscode" ] = appdata / "Code" / "User" / "keybindings.json"
7095
7196 # Windows Terminal path is tricky (has a unique hash in folder name)
7297 # We look for the folder starting with Microsoft.WindowsTerminal
@@ -77,6 +102,10 @@ def _get_config_paths(cls):
77102 if wt_glob :
78103 paths ["windows_terminal" ] = wt_glob [0 ]
79104
105+ else : # Linux
106+ # VS Code on Linux
107+ paths ["vscode" ] = home / ".config" / "Code" / "User" / "keybindings.json"
108+
80109 return paths
81110
82111 @classmethod
@@ -325,6 +354,133 @@ def _update_windows_terminal(cls, path, io, dry_run=False):
325354 )
326355 return False
327356
357+ @classmethod
358+ def _update_vscode (cls , path , io , dry_run = False ):
359+ """Updates VS Code keybindings.json with shift+enter binding."""
360+ # Create directory if it doesn't exist
361+ path .parent .mkdir (parents = True , exist_ok = True )
362+
363+ if not path .exists ():
364+ if dry_run :
365+ io .tool_output (f"DRY-RUN: VS Code keybindings.json doesn't exist at { path } " )
366+ io .tool_output (
367+ "DRY-RUN: Would create file with binding:"
368+ f" { json .dumps (cls .VSCODE_SHIFT_ENTER_BINDING , indent = 2 )} "
369+ )
370+ return True
371+ else :
372+ io .tool_output (f"Creating VS Code keybindings.json at { path } " )
373+ # Create file with our binding
374+ data = [cls .VSCODE_SHIFT_ENTER_BINDING ]
375+ with open (path , "w" , encoding = "utf-8" ) as f :
376+ json .dump (data , f , indent = 4 )
377+ io .tool_output ("Created VS Code config with shift+enter binding." )
378+ return True
379+
380+ if dry_run :
381+ io .tool_output (f"DRY-RUN: Would check VS Code keybindings at { path } " )
382+ io .tool_output (
383+ "DRY-RUN: Would add binding:"
384+ f" { json .dumps (cls .VSCODE_SHIFT_ENTER_BINDING , indent = 2 )} "
385+ )
386+ # Simulate checking for duplicates
387+ try :
388+ content = ""
389+ with open (path , "r" , encoding = "utf-8" ) as f :
390+ content = f .read ()
391+
392+ # Strip comments before parsing
393+ content_no_comments = cls ._strip_json_comments (content )
394+ if content_no_comments .strip ():
395+ data = json .loads (content_no_comments )
396+ else :
397+ data = []
398+
399+ # Check if binding already exists
400+ already_exists = False
401+ if isinstance (data , list ):
402+ for binding in data :
403+ if isinstance (binding , dict ):
404+ # Check if this is our shift+enter binding
405+ if (
406+ binding .get ("key" ) == "shift+enter"
407+ and binding .get ("command" )
408+ == "workbench.action.terminal.sendSequence"
409+ and binding .get ("when" ) == "terminalFocus"
410+ ):
411+ already_exists = True
412+ break
413+
414+ if already_exists :
415+ io .tool_output ("DRY-RUN: VS Code already configured." )
416+ return False
417+ else :
418+ io .tool_output ("DRY-RUN: Would update VS Code config." )
419+ return True
420+ except json .JSONDecodeError :
421+ io .tool_output (
422+ "DRY-RUN: Error: Could not parse VS Code keybindings.json. Is it valid JSON?"
423+ )
424+ return False
425+ except Exception as e :
426+ io .tool_output (f"DRY-RUN: Error reading file: { e } " )
427+ return False
428+
429+ cls ._backup_file (path , io )
430+
431+ try :
432+ content = ""
433+ with open (path , "r" , encoding = "utf-8" ) as f :
434+ content = f .read ()
435+
436+ # Strip comments before parsing
437+ content_no_comments = cls ._strip_json_comments (content )
438+ if content_no_comments .strip ():
439+ data = json .loads (content_no_comments )
440+ else :
441+ data = []
442+
443+ # Ensure data is a list
444+ if not isinstance (data , list ):
445+ io .tool_output (
446+ "Error: VS Code keybindings.json should contain an array of keybindings."
447+ )
448+ return False
449+
450+ # Check if binding already exists
451+ already_exists = False
452+ for binding in data :
453+ if isinstance (binding , dict ):
454+ # Check if this is our shift+enter binding
455+ if (
456+ binding .get ("key" ) == "shift+enter"
457+ and binding .get ("command" ) == "workbench.action.terminal.sendSequence"
458+ and binding .get ("when" ) == "terminalFocus"
459+ ):
460+ already_exists = True
461+ break
462+
463+ if already_exists :
464+ io .tool_output ("VS Code already configured." )
465+ return False
466+
467+ # Add our binding
468+ data .append (cls .VSCODE_SHIFT_ENTER_BINDING )
469+
470+ # Write back to file
471+ with open (path , "w" , encoding = "utf-8" ) as f :
472+ json .dump (data , f , indent = 4 )
473+
474+ io .tool_output ("Updated VS Code config." )
475+ return True
476+
477+ except json .JSONDecodeError :
478+ io .tool_output ("Error: Could not parse VS Code keybindings.json. Is it valid JSON?" )
479+ return False
480+ except Exception as e :
481+ io .tool_output (f"Error updating VS Code config: { e } " )
482+ return False
483+
328484 @classmethod
329485 async def execute (cls , io , coder , args , ** kwargs ):
330486 """Configure terminal config files to support shift+enter for newline."""
@@ -350,6 +506,10 @@ async def execute(cls, io, coder, args, **kwargs):
350506 if cls ._update_windows_terminal (paths ["windows_terminal" ], io , dry_run = dry_run ):
351507 updated = True
352508
509+ if "vscode" in paths :
510+ if cls ._update_vscode (paths ["vscode" ], io , dry_run = dry_run ):
511+ updated = True
512+
353513 if dry_run :
354514 if updated :
355515 io .tool_output (
@@ -392,7 +552,7 @@ def get_help(cls) -> str:
392552 )
393553 help_text += (
394554 "\n Note: This command modifies terminal configuration files (Alacritty, Kitty, Windows"
395- " Terminal)\n "
555+ " Terminal, VS Code )\n "
396556 )
397557 help_text += (
398558 "to add a key binding that sends a newline character when shift+enter is pressed.\n "
0 commit comments