{ config, lib, pkgs, ... }: with lib; let cfg = config.programs.vscode; vscodePname = cfg.package.pname; vscodeVersion = cfg.package.version; jsonFormat = pkgs.formats.json { }; configDir = { "vscode" = "Code"; "vscode-insiders" = "Code - Insiders"; "vscodium" = "VSCodium"; "openvscode-server" = "OpenVSCode Server"; }.${vscodePname}; extensionDir = { "vscode" = "vscode"; "vscode-insiders" = "vscode-insiders"; "vscodium" = "vscode-oss"; "openvscode-server" = "openvscode-server"; }.${vscodePname}; userDir = if pkgs.stdenv.hostPlatform.isDarwin then "Library/Application Support/${configDir}/User" else "${config.xdg.configHome}/${configDir}/User"; 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 = name: "${userDir}/${ optionalString (name != "default") "profiles/${name}/" }snippets"; # TODO: On Darwin where are the extensions? extensionPath = ".${extensionDir}/extensions"; 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 = userSettings: enableUpdateCheck: enableExtensionUpdateCheck: userSettings // optionalAttrs (enableUpdateCheck == false) { "update.mode" = "none"; } // optionalAttrs (enableExtensionUpdateCheck == false) { "extensions.autoCheckUpdates" = false; }; profileType = 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."; }; enableUpdateCheck = mkOption { type = types.nullOr types.bool; default = null; description = '' Whether to enable update checks/notifications. Can only be set for the default profile, but it applies to all profiles. ''; }; enableExtensionUpdateCheck = mkOption { type = types.nullOr types.bool; default = null; description = '' Whether to enable update notifications for extensions. Can only be set for the default profile, but it applies to all profiles. ''; }; }; }; defaultProfile = (filterAttrs (n: v: n == "default") cfg.profiles).default; allProfilesExceptDefault = (removeAttrs cfg.profiles [ "default" ]); in { imports = [ (mkChangedOptionModule [ "programs" "vscode" "immutableExtensionsDir" ] [ "programs" "vscode" "mutableExtensionsDir" ] (config: !config.programs.vscode.immutableExtensionsDir)) ] ++ map (v: mkRenamedOptionModule [ "programs" "vscode" v ] [ "programs" "vscode" "profiles" "default" v ]) [ "enableUpdateCheck" "enableExtensionUpdateCheck" "userSettings" "userTasks" "keybindings" "extensions" "languageSnippets" "globalSnippets" ]; 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. ''; }; mutableExtensionsDir = mkOption { type = types.bool; default = allProfilesExceptDefault == { }; example = false; description = '' Whether extensions can be installed or updated manually or by Visual Studio Code. Mutually exclusive to programs.vscode.profiles. ''; }; profiles = mkOption { type = types.attrsOf profileType; default = { }; description = '' A list of all VSCode profiles. Mutually exclusive to programs.vscode.mutableExtensionsDir ''; }; }; config = mkIf cfg.enable { warnings = [ (mkIf (allProfilesExceptDefault != { } && cfg.mutableExtensionsDir) "programs.vscode.mutableExtensionsDir can be used only if no profiles apart from default are set.") (mkIf ((filterAttrs (n: v: (v ? enableExtensionUpdateCheck || v ? enableUpdateCheck) && (v.enableExtensionUpdateCheck != null || v.enableUpdateCheck != null)) allProfilesExceptDefault) != { }) "The option programs.vscode.profiles.*.enableExtensionUpdateCheck and option programs.vscode.profiles.*.enableUpdateCheck is invalid for all profiles except default.") ]; home.packages = [ cfg.package ]; # The file `${userDir}/globalStorage/storage.json` needs to be writable by VSCode, # since it contains other data, such as theme backgrounds, recently opened folders, etc. # A caveat of adding profiles this way is, VSCode has to be closed # when this file is being written, since the file is loaded into RAM # and overwritten on closing VSCode. home.activation.vscodeProfiles = hm.dag.entryAfter [ "writeBoundary" ] (let modifyGlobalStorage = pkgs.writeShellScript "vscode-global-storage-modify" '' PATH=${makeBinPath [ pkgs.jq ]}''${PATH:+:}$PATH file="${userDir}/globalStorage/storage.json" if [ -f "$file" ]; then existing_profiles=$(jq '.userDataProfiles // [] | map({ (.name): .location }) | add // {}' $file) file_write="" profiles=(${ escapeShellArgs (flatten (mapAttrsToList (n: v: n) allProfilesExceptDefault)) }) for profile in "''${profiles[@]}"; do if [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" != "true" ]] || [[ "$(echo $existing_profiles | jq --arg profile $profile 'has ($profile)')" == "true" && "$(echo $existing_profiles | jq --arg profile $profile '.[$profile]')" != "\"$profile\"" ]]; then file_write="$file_write$([ "$file_write" != "" ] && echo "...")$profile" fi done else for profile in "''${profiles[@]}"; do file_write="$file_write$([ "$file_write" != "" ] && echo "...")$profile" done echo "{}" > $file fi if [ "$file_write" != "" ]; then userDataProfiles=$(jq ".userDataProfiles += $(echo $file_write | jq -R 'split("...") | map({ name: ., location: . })')" $file) echo $userDataProfiles > $file fi ''; in modifyGlobalStorage.outPath); home.file = mkMerge (flatten [ (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); }) (mkIf (v.userTasks != { }) { "${tasksFilePath n}".source = jsonFormat.generate "vscode-user-tasks" v.userTasks; }) (mkIf (v.keybindings != [ ]) { "${keybindingsFilePath n}".source = jsonFormat.generate "vscode-keybindings" (map (filterAttrs (_: v: v != null)) v.keybindings); }) (mkIf (v.languageSnippets != { }) (mapAttrs' (language: snippet: nameValuePair "${snippetDir n}/${language}.json" { source = jsonFormat.generate "user-snippet-${language}.json" snippet; }) v.languageSnippets)) (mkIf (v.globalSnippets != { }) { "${snippetDir n}/global.code-snippets".source = jsonFormat.generate "user-snippet-global.code-snippets" v.globalSnippets; }) ]) cfg.profiles) # We write extensions.json for all profiles, except the default profile, # since that is handled by code below. (mkIf (allProfilesExceptDefault != { }) (mapAttrs' (n: v: nameValuePair "${userDir}/profiles/${n}/extensions.json" { source = "${ extensionJsonFile n (extensionJson v.extensions) }/share/vscode/extensions/extensions.json"; }) allProfilesExceptDefault)) (mkIf (cfg.profiles != { }) (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 && allProfilesExceptDefault == { }) then # Mutable extensions dir can only occur when only default profile is set. # Force regenerating extensions.json using the below method, # causes VSCode to create the extensions.json with all the extensions # in the extension directory, which includes extensions from other profiles. mkMerge (concatMap toPaths (flatten (mapAttrsToList (n: v: v.extensions) cfg.profiles)) ++ optional (versionAtLeast vscodeVersion "1.74.0" && defaultProfile != { }) { # Whenever our immutable extensions.json changes, force VSCode to regenerate # extensions.json with both mutable and immutable extensions. "${extensionPath}/.extensions-immutable.json" = { text = extensionJson defaultProfile.extensions; onChange = '' run rm $VERBOSE_ARG -f ${extensionPath}/{extensions.json,.init-default-profile-extensions} verboseEcho "Regenerating VSCode extensions.json" run ${getExe cfg.package} --list-extensions > /dev/null ''; }; }) else { "${extensionPath}".source = let combinedExtensionsDrv = pkgs.buildEnv { name = "vscode-extensions"; paths = (flatten (mapAttrsToList (n: v: v.extensions) cfg.profiles)) ++ optional (versionAtLeast vscodeVersion "1.74.0" && defaultProfile != { }) (extensionJsonFile "default" (extensionJson defaultProfile.extensions)); }; in "${combinedExtensionsDrv}/${subDir}"; })) ]); }; }