From 6266e351d49a937447f6e527b5e81c6e226d0ab5 Mon Sep 17 00:00:00 2001 From: Reputable2722 <153411261+Reputable2772@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:39:45 +0530 Subject: [PATCH] vscode: add initial profiles support Signed-off-by: Reputable2722 <153411261+Reputable2772@users.noreply.github.com> --- modules/programs/vscode.nix | 472 ++++++++++++++++++++---------------- 1 file changed, 269 insertions(+), 203 deletions(-) diff --git a/modules/programs/vscode.nix b/modules/programs/vscode.nix index f77937d66..ef868cbb2 100644 --- a/modules/programs/vscode.nix +++ b/modules/programs/vscode.nix @@ -1,5 +1,7 @@ { config, lib, pkgs, ... }: +# TODO: Re-write tests to support profiles. + with lib; let @@ -30,28 +32,174 @@ let else "${config.xdg.configHome}/${configDir}/User"; - configFilePath = "${userDir}/settings.json"; - tasksFilePath = "${userDir}/tasks.json"; - keybindingsFilePath = "${userDir}/keybindings.json"; + configFilePath = name: + "${userDir}/${ + optionalString (name != "default") "profiles/${name}/" + }settings.json"; + tasksFilePath = name: + "${userDir}/${ + optionalString (name != "default") "profiles/${name}/" + }tasks.json"; + keybindingsFilePath = name: + "${userDir}/${ + optionalString (name != "default") "profiles/${name}/" + }keybindings.json"; - snippetDir = "${userDir}/snippets"; + snippetDir = name: + "${userDir}/${ + optionalString (name != "default") "profiles/${name}/" + }snippets"; # TODO: On Darwin where are the extensions? extensionPath = ".${extensionDir}/extensions"; - extensionJson = pkgs.vscode-utils.toExtensionJson cfg.extensions; - extensionJsonFile = pkgs.writeTextFile { - name = "extensions-json"; - destination = "/share/vscode/extensions/extensions.json"; - text = extensionJson; - }; + extensionJson = ext: pkgs.vscode-utils.toExtensionJson ext; + extensionJsonFile = name: text: + pkgs.writeTextFile { + inherit text; + name = "extensions-json-${name}"; + destination = "/share/vscode/extensions/extensions.json"; + }; - mergedUserSettings = cfg.userSettings + mergedUserSettings = userSettings: + userSettings // optionalAttrs (!cfg.enableUpdateCheck) { "update.mode" = "none"; } // optionalAttrs (!cfg.enableExtensionUpdateCheck) { "extensions.autoCheckUpdates" = false; }; + + profileType = default: + types.submodule { + options = { + userSettings = mkOption { + type = jsonFormat.type; + default = { }; + example = literalExpression '' + { + "files.autoSave" = "off"; + "[nix]"."editor.tabSize" = 2; + } + ''; + description = '' + Configuration written to Visual Studio Code's + {file}`settings.json`. + ''; + }; + + userTasks = mkOption { + type = jsonFormat.type; + default = { }; + example = literalExpression '' + { + version = "2.0.0"; + tasks = [ + { + type = "shell"; + label = "Hello task"; + command = "hello"; + } + ]; + } + ''; + description = '' + Configuration written to Visual Studio Code's + {file}`tasks.json`. + ''; + }; + + 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"; }; + description = "Optional arguments for a command."; + }; + }; + }); + default = [ ]; + example = literalExpression '' + [ + { + key = "ctrl+c"; + command = "editor.action.clipboardCopyAction"; + when = "textInputFocus"; + } + ] + ''; + description = '' + Keybindings written to Visual Studio Code's + {file}`keybindings.json`. + ''; + }; + + extensions = mkOption { + type = types.listOf types.package; + default = [ ]; + example = literalExpression "[ pkgs.vscode-extensions.bbenoist.nix ]"; + description = '' + The extensions Visual Studio Code should be started with. + ''; + }; + + languageSnippets = mkOption { + type = jsonFormat.type; + default = { }; + example = { + haskell = { + fixme = { + prefix = [ "fixme" ]; + body = [ "$LINE_COMMENT FIXME: $0" ]; + description = "Insert a FIXME remark"; + }; + }; + }; + description = "Defines user snippets for different languages."; + }; + + globalSnippets = mkOption { + type = jsonFormat.type; + default = { }; + example = { + fixme = { + prefix = [ "fixme" ]; + body = [ "$LINE_COMMENT FIXME: $0" ]; + description = "Insert a FIXME remark"; + }; + }; + description = "Defines global user snippets."; + }; + } // optionalAttrs default { + name = mkOption { + type = types.str; + description = "Visual Studio Code's Profile name."; + }; + }; + }; + allProfiles = cfg.profiles ++ [ cfg.defaultProfile ]; in { + # TODO: Backwards compatibility with old options. imports = [ (mkChangedOptionModule [ "programs" "vscode" "immutableExtensionsDir" ] [ "programs" @@ -60,204 +208,134 @@ in { ] (config: !config.programs.vscode.immutableExtensionsDir)) ]; - options = { - programs.vscode = { - enable = mkEnableOption "Visual Studio Code"; + options.programs.vscode = { + enable = mkEnableOption "Visual Studio Code"; - package = mkOption { - type = types.package; - default = pkgs.vscode; - defaultText = literalExpression "pkgs.vscode"; - example = literalExpression "pkgs.vscodium"; - description = '' - Version of Visual Studio Code to install. - ''; - }; + package = mkOption { + type = types.package; + default = pkgs.vscode; + defaultText = literalExpression "pkgs.vscode"; + example = literalExpression "pkgs.vscodium"; + description = '' + Version of Visual Studio Code to install. + ''; + }; - enableUpdateCheck = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable update checks/notifications. - ''; - }; + enableUpdateCheck = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable update checks/notifications. + ''; + }; - enableExtensionUpdateCheck = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable update notifications for extensions. - ''; - }; + enableExtensionUpdateCheck = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable update notifications for extensions. + ''; + }; - userSettings = mkOption { - type = jsonFormat.type; - default = { }; - example = literalExpression '' - { - "files.autoSave" = "off"; - "[nix]"."editor.tabSize" = 2; - } - ''; - description = '' - Configuration written to Visual Studio Code's - {file}`settings.json`. - ''; - }; + mutableExtensionsDir = mkOption { + type = types.bool; + default = cfg.profiles == [ ]; + example = false; + description = '' + Whether extensions can be installed or updated manually + or by Visual Studio Code. This option is effective only + when there is a single profile (i.e. default). + ''; + }; - userTasks = mkOption { - type = jsonFormat.type; - default = { }; - example = literalExpression '' - { - version = "2.0.0"; - tasks = [ - { - type = "shell"; - label = "Hello task"; - command = "hello"; - } - ]; - } - ''; - description = '' - Configuration written to Visual Studio Code's - {file}`tasks.json`. - ''; - }; - - 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"; }; - description = "Optional arguments for a command."; - }; - }; - }); - default = [ ]; - example = literalExpression '' - [ - { - key = "ctrl+c"; - command = "editor.action.clipboardCopyAction"; - when = "textInputFocus"; - } - ] - ''; - description = '' - Keybindings written to Visual Studio Code's - {file}`keybindings.json`. - ''; - }; - - extensions = mkOption { - type = types.listOf types.package; - default = [ ]; - example = literalExpression "[ pkgs.vscode-extensions.bbenoist.nix ]"; - description = '' - The extensions Visual Studio Code should be started with. - ''; - }; - - mutableExtensionsDir = mkOption { - type = types.bool; - default = true; - example = false; - description = '' - Whether extensions can be installed or updated manually - or by Visual Studio Code. - ''; - }; - - languageSnippets = mkOption { - type = jsonFormat.type; - default = { }; - example = { - haskell = { - fixme = { - prefix = [ "fixme" ]; - body = [ "$LINE_COMMENT FIXME: $0" ]; - description = "Insert a FIXME remark"; - }; - }; - }; - description = "Defines user snippets for different languages."; - }; - - globalSnippets = mkOption { - type = jsonFormat.type; - default = { }; - example = { - fixme = { - prefix = [ "fixme" ]; - body = [ "$LINE_COMMENT FIXME: $0" ]; - description = "Insert a FIXME remark"; - }; - }; - description = "Defines global user snippets."; - }; + profiles = mkOption { + type = types.listOf (profileType true); + default = [ ]; + }; + defaultProfile = mkOption { + type = profileType false; + default = { }; }; }; config = mkIf cfg.enable { + warnings = [ + (mkIf (cfg.profiles != [ ] && cfg.mutableExtensionsDir) + "programs.vscode.mutableExtensionsDir can be used only if profiles is an empty list.") + ]; + home.packages = [ cfg.package ]; - home.file = mkMerge [ - (mkIf (mergedUserSettings != { }) { - "${configFilePath}".source = - jsonFormat.generate "vscode-user-settings" mergedUserSettings; - }) - (mkIf (cfg.userTasks != { }) { - "${tasksFilePath}".source = - jsonFormat.generate "vscode-user-tasks" cfg.userTasks; - }) - (mkIf (cfg.keybindings != [ ]) - (let dropNullFields = filterAttrs (_: v: v != null); - in { - "${keybindingsFilePath}".source = - jsonFormat.generate "vscode-keybindings" - (map dropNullFields cfg.keybindings); - })) - (mkIf (cfg.extensions != [ ]) (let - subDir = "share/vscode/extensions"; + /* * + TODO: Write a home.activation script for ${userDir}/globalStorage/storage.json, appending + every profile in the format `{ "name": , "location": }` to the + userDataProfiles array. + This file needs to mutable, and cannot be symlinked. This is because the file stores other data, + such as background themes, keybindingReferences, etc. + */ + + home.file = mkMerge (flatten [ + (map (v: + let + # The default profile does not have the `name` key + name = if v ? name then v.name else "default"; + in [ + (mkIf ((mergedUserSettings v.userSettings) != { }) { + "${configFilePath name}".source = + jsonFormat.generate "vscode-user-settings" + (mergedUserSettings v.userSettings); + }) + + (mkIf (v.userTasks != { }) { + "${tasksFilePath name}".source = + jsonFormat.generate "vscode-user-tasks" v.userTasks; + }) + + (mkIf (v.keybindings != [ ]) { + "${keybindingsFilePath name}".source = + jsonFormat.generate "vscode-keybindings" + (map (filterAttrs (_: v: v != null)) v.keybindings); + }) + + (mkIf (v.languageSnippets != { }) (lib.mapAttrs' (language: snippet: + lib.nameValuePair "${snippetDir name}/${language}.json" { + source = + jsonFormat.generate "user-snippet-${language}.json" snippet; + }) v.languageSnippets)) + + (mkIf (v.globalSnippets != { }) { + "${snippetDir name}/global.code-snippets".source = + jsonFormat.generate "user-snippet-global.code-snippets" + v.globalSnippets; + }) + ]) allProfiles) + + # We write extensions.json for all profiles, except the default profile, + # since that is handled by code below. + (mkIf (cfg.profiles != [ ]) (listToAttrs (map (v: + nameValuePair "${userDir}/profiles/${v.name}/extensions.json" { + source = "${ + extensionJsonFile v.name (extensionJson v.extensions) + }/share/vscode/extensions/extensions.json"; + }) cfg.profiles))) + + (mkIf ((filter (v: v.extensions != [ ]) allProfiles) != [ ]) (let # Adapted from https://discourse.nixos.org/t/vscode-extensions-setup/1801/2 + subDir = "share/vscode/extensions"; toPaths = ext: map (k: { "${extensionPath}/${k}".source = "${ext}/${subDir}/${k}"; }) (if ext ? vscodeExtUniqueId then [ ext.vscodeExtUniqueId ] else builtins.attrNames (builtins.readDir (ext + "/${subDir}"))); - in if cfg.mutableExtensionsDir then - mkMerge (concatMap toPaths cfg.extensions + in if (cfg.mutableExtensionsDir && cfg.profiles == [ ]) then + mkMerge (concatMap toPaths (flatten (map (v: v.extensions) allProfiles)) ++ lib.optional (lib.versionAtLeast vscodeVersion "1.74.0") { # Whenever our immutable extensions.json changes, force VSCode to regenerate # extensions.json with both mutable and immutable extensions. "${extensionPath}/.extensions-immutable.json" = { - text = extensionJson; + text = extensionJson cfg.defaultProfile.extensions; onChange = '' run rm $VERBOSE_ARG -f ${extensionPath}/{extensions.json,.init-default-profile-extensions} verboseEcho "Regenerating VSCode extensions.json" @@ -269,25 +347,13 @@ in { "${extensionPath}".source = let combinedExtensionsDrv = pkgs.buildEnv { name = "vscode-extensions"; - paths = cfg.extensions - ++ lib.optional (lib.versionAtLeast vscodeVersion "1.74.0") - extensionJsonFile; + paths = flatten (map (v: v.extensions) allProfiles) ++ lib.optional + (lib.versionAtLeast vscodeVersion "1.74.0" && cfg.defaultProfile + != { }) (extensionJsonFile "default" + (extensionJson cfg.defaultProfile.extensions)); }; in "${combinedExtensionsDrv}/${subDir}"; })) - - (mkIf (cfg.globalSnippets != { }) - (let globalSnippets = "${snippetDir}/global.code-snippets"; - in { - "${globalSnippets}".source = - jsonFormat.generate "user-snippet-global.code-snippets" - cfg.globalSnippets; - })) - - (lib.mapAttrs' (language: snippet: - lib.nameValuePair "${snippetDir}/${language}.json" { - source = jsonFormat.generate "user-snippet-${language}.json" snippet; - }) cfg.languageSnippets) - ]; + ]); }; }