From b022c9e3b805484e44aaad1973ed7572b0c1b5c4 Mon Sep 17 00:00:00 2001 From: Jonathan Bouchard Date: Thu, 15 May 2025 14:32:46 -0400 Subject: [PATCH] vscode: allow specifying paths for VSCode JSON settings (#7055) Enables users to provide paths to JSON files for VS Code settings, tasks, and keybindings. This allows for more flexible configuration management and reuse of existing configuration files instead of using inline configurations. --- modules/programs/vscode.nix | 94 +++++++++++-------- tests/modules/programs/vscode/keybindings.nix | 44 +++++++++ tests/modules/programs/vscode/tasks.nix | 27 ++++++ 3 files changed, 125 insertions(+), 40 deletions(-) diff --git a/modules/programs/vscode.nix b/modules/programs/vscode.nix index 104399db7..444b19328 100644 --- a/modules/programs/vscode.nix +++ b/modules/programs/vscode.nix @@ -81,10 +81,12 @@ let "extensions.autoCheckUpdates" = false; }; + isPath = p: builtins.isPath p || lib.isStorePath p; + profileType = types.submodule { options = { userSettings = mkOption { - type = jsonFormat.type; + type = types.either types.path jsonFormat.type; default = { }; example = literalExpression '' { @@ -95,11 +97,12 @@ let description = '' Configuration written to Visual Studio Code's {file}`settings.json`. + This can be a JSON object or a path to a custom JSON file. ''; }; userTasks = mkOption { - type = jsonFormat.type; + type = types.either types.path jsonFormat.type; default = { }; example = literalExpression '' { @@ -116,43 +119,46 @@ let description = '' Configuration written to Visual Studio Code's {file}`tasks.json`. + This can be a JSON object or a path to a custom JSON file. ''; }; keybindings = mkOption { - type = types.listOf ( - types.submodule { - options = { - key = mkOption { - type = types.str; - example = "ctrl+c"; - description = "The key or key-combination to bind."; - }; - - command = mkOption { - type = types.str; - example = "editor.action.clipboardCopyAction"; - description = "The VS Code command to execute."; - }; - - when = mkOption { - type = types.nullOr (types.str); - default = null; - example = "textInputFocus"; - description = "Optional context filter."; - }; - - # https://code.visualstudio.com/docs/getstarted/keybindings#_command-arguments - args = mkOption { - type = types.nullOr (jsonFormat.type); - default = null; - example = { - direction = "up"; + type = types.either types.path ( + types.listOf ( + types.submodule { + options = { + key = mkOption { + type = types.str; + example = "ctrl+c"; + description = "The key or key-combination to bind."; + }; + + command = mkOption { + type = types.str; + example = "editor.action.clipboardCopyAction"; + description = "The VS Code command to execute."; + }; + + when = mkOption { + type = types.nullOr (types.str); + default = null; + example = "textInputFocus"; + description = "Optional context filter."; + }; + + # https://code.visualstudio.com/docs/getstarted/keybindings#_command-arguments + args = mkOption { + type = types.nullOr (jsonFormat.type); + default = null; + example = { + direction = "up"; + }; + description = "Optional arguments for a command."; }; - description = "Optional arguments for a command."; }; - }; - } + } + ) ); default = [ ]; example = literalExpression '' @@ -167,6 +173,7 @@ let description = '' Keybindings written to Visual Studio Code's {file}`keybindings.json`. + This can be a JSON object or a path to a custom JSON file. ''; }; @@ -360,20 +367,27 @@ in (mapAttrsToList (n: v: [ (mkIf ((mergedUserSettings v.userSettings v.enableUpdateCheck v.enableExtensionUpdateCheck) != { }) { - "${configFilePath n}".source = jsonFormat.generate "vscode-user-settings" ( - mergedUserSettings v.userSettings v.enableUpdateCheck v.enableExtensionUpdateCheck - ); + "${configFilePath n}".source = + if isPath v.userSettings then + v.userSettings + else + jsonFormat.generate "vscode-user-settings" ( + mergedUserSettings v.userSettings v.enableUpdateCheck v.enableExtensionUpdateCheck + ); } ) (mkIf (v.userTasks != { }) { - "${tasksFilePath n}".source = jsonFormat.generate "vscode-user-tasks" v.userTasks; + "${tasksFilePath n}".source = + if isPath v.userTasks then v.userTasks else jsonFormat.generate "vscode-user-tasks" v.userTasks; }) (mkIf (v.keybindings != [ ]) { - "${keybindingsFilePath n}".source = jsonFormat.generate "vscode-keybindings" ( - map (lib.filterAttrs (_: v: v != null)) v.keybindings - ); + "${keybindingsFilePath n}".source = + if isPath v.keybindings then + v.keybindings + else + jsonFormat.generate "vscode-keybindings" (map (lib.filterAttrs (_: v: v != null)) v.keybindings); }) (mkIf (v.languageSnippets != { }) ( diff --git a/tests/modules/programs/vscode/keybindings.nix b/tests/modules/programs/vscode/keybindings.nix index a48a7f413..b27d7520e 100644 --- a/tests/modules/programs/vscode/keybindings.nix +++ b/tests/modules/programs/vscode/keybindings.nix @@ -45,6 +45,42 @@ let else ".config/Code/User/${lib.optionalString (name != "default") "profiles/${name}/"}settings.json"; + content = '' + [ + // Order doesn't change + { + "command": "deleteFile", + "key": "ctrl+c", + "when": "" + }, + { + "command": "deleteFile", + "key": "ctrl+c", + "when": "" + }, + { + "args": { + "command": "echo file" + }, + "command": "run", + "key": "ctrl+r" + }, + // Comments should be preserved + { + "command": "editor.action.clipboardCopyAction", + "key": "ctrl+c", + "when": "textInputFocus && false" + }, + { + "command": "deleteFile", + "key": "d", + "when": "explorerViewletVisible" + } + ] + ''; + + customBindingsPath = pkgs.writeText "custom.json" content; + expectedKeybindings = pkgs.writeText "expected.json" '' [ { @@ -72,6 +108,8 @@ let ] ''; + expectedCustomKeybindings = pkgs.writeText "custom-expected.json" content; + in { programs.vscode = { @@ -79,6 +117,7 @@ in profiles = { default.keybindings = bindings; test.keybindings = bindings; + custom.keybindings = customBindingsPath; }; package = pkgs.writeScriptBin "vscode" "" // { pname = "vscode"; @@ -96,5 +135,10 @@ in assertFileContent "home-files/${keybindingsPath "test"}" "${expectedKeybindings}" assertPathNotExists "home-files/${settingsPath "test"}" + + assertFileExists "home-files/${keybindingsPath "custom"}" + assertFileContent "home-files/${keybindingsPath "custom"}" "${expectedCustomKeybindings}" + + assertPathNotExists "home-files/${settingsPath "custom"}" ''; } diff --git a/tests/modules/programs/vscode/tasks.nix b/tests/modules/programs/vscode/tasks.nix index ec03350f2..aa9ab8a52 100644 --- a/tests/modules/programs/vscode/tasks.nix +++ b/tests/modules/programs/vscode/tasks.nix @@ -11,6 +11,25 @@ let else ".config/Code/User/${lib.optionalString (name != "default") "profiles/${name}/"}tasks.json"; + content = '' + { + // Comments should be preserved + "tasks": [ + { + "command": "hello", + "label": "Hello task", + "type": "shell" + }, + { + "command": "world", + "label": "World task", + "type": "shell" + } + ], + "version": "2.0.0" + } + ''; + tasks = { version = "2.0.0"; tasks = [ @@ -22,6 +41,8 @@ let ]; }; + customTasksPath = pkgs.writeText "custom.json" content; + expectedTasks = pkgs.writeText "tasks-expected.json" '' { "tasks": [ @@ -35,6 +56,8 @@ let } ''; + expectedCustomTasks = pkgs.writeText "custom-expected.json" content; + in { programs.vscode = { @@ -46,6 +69,7 @@ in profiles = { default.userTasks = tasks; test.userTasks = tasks; + custom.userTasks = customTasksPath; }; }; @@ -55,5 +79,8 @@ in assertFileExists "home-files/${tasksFilePath "test"}" assertFileContent "home-files/${tasksFilePath "test"}" "${expectedTasks}" + + assertFileExists "home-files/${tasksFilePath "custom"}" + assertFileContent "home-files/${tasksFilePath "custom"}" "${expectedCustomTasks}" ''; }