{ config, lib, pkgs, ... }: let inherit (lib) literalExpression mkIf mkMerge mkOption types ; cfg = config.programs.zed-editor; jsonFormat = pkgs.formats.json { }; json5 = pkgs.python3Packages.toPythonApplication pkgs.python3Packages.json5; transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) ( lib.mapAttrs ( name: server: # See: # # - https://zed.dev/docs/ai/mcp # - https://github.com/zed-industries/zed/discussions/53780 # - https://github.com/zed-industries/zed/blob/v1.6.3/crates/project/src/project_settings.rs#L182 (lib.optionalAttrs (server.command != null) { args = [ ]; }) // (lib.hm.mcp.transformMcpServer { inherit server; extraTransforms = [ (lib.hm.mcp.wrapEnvFilesCommand { inherit pkgs name; }) ]; }) ) config.programs.mcp.servers ); settingMcpServers = lib.attrByPath [ "context_servers" ] { } cfg.userSettings; mergedMcpServers = transformedMcpServers // settingMcpServers; mergedSettings = cfg.userSettings // (lib.optionalAttrs (builtins.length cfg.extensions > 0) { # this part by @cmacrae auto_install_extensions = lib.genAttrs cfg.extensions (_: true); }) // (lib.optionalAttrs (mergedMcpServers != { }) { context_servers = mergedMcpServers; }); # Files linked from the store instead of merged. The cleanup reads the # matching file entries, so both must use these conditions. immutableFiles = { settings = !cfg.mutableUserSettings && mergedSettings != { }; keymap = !cfg.mutableUserKeymaps && cfg.userKeymaps != [ ]; tasks = !cfg.mutableUserTasks && cfg.userTasks != [ ]; debug = !cfg.mutableUserDebug && cfg.userDebug != [ ]; }; immutableConfigCleanup = lib.concatMapStrings ( name: lib.hm.generators.mkImpureConfigCleanup { file = config.home.file."${config.xdg.configHome}/zed/${name}.json"; } ) (lib.attrNames (lib.filterAttrs (_: lib.id) immutableFiles)); editorEnv = { EDITOR = "${cfg.package.meta.mainProgram} --wait"; VISUAL = "${cfg.package.meta.mainProgram} --wait"; }; in { meta.maintainers = [ lib.maintainers.alinnow lib.maintainers.zh4ngx ]; options = { programs.zed-editor = { enable = lib.mkEnableOption "Zed, the high performance, multiplayer code editor from the creators of Atom and Tree-sitter"; package = lib.mkPackageOption pkgs "zed-editor" { nullable = true; }; extraPackages = mkOption { type = with types; listOf package; default = [ ]; example = literalExpression "[ pkgs.nixd ]"; description = "Extra packages available to Zed."; }; mutableUserSettings = mkOption { type = types.bool; default = true; example = false; description = '' Whether user settings (settings.json) can be updated by zed. ''; }; mutableUserKeymaps = mkOption { type = types.bool; default = true; example = false; description = '' Whether user keymaps (keymap.json) can be updated by zed. ''; }; mutableUserTasks = mkOption { type = types.bool; default = true; example = false; description = '' Whether user tasks (tasks.json) can be updated by zed. ''; }; mutableUserDebug = mkOption { type = types.bool; default = true; example = false; description = '' Whether user debug configurations (debug.json) can be updated by zed. ''; }; userSettings = mkOption { inherit (jsonFormat) type; default = { }; example = { features = { copilot = false; }; telemetry = { metrics = false; }; vim_mode = false; ui_font_size = 16; buffer_font_size = 16; }; description = '' Configuration written to Zed's {file}`settings.json`. ''; }; userKeymaps = mkOption { inherit (jsonFormat) type; default = [ ]; example = literalExpression '' [ { context = "Workspace"; bindings = { ctrl-shift-t = "workspace::NewTerminal"; }; }; ] ''; description = '' Configuration written to Zed's {file}`keymap.json`. ''; }; userTasks = mkOption { inherit (jsonFormat) type; default = [ ]; example = [ { label = "Format Code"; command = "nix"; args = [ "fmt" "$ZED_WORKTREE_ROOT" ]; } ]; description = '' Configuration written to Zed's {file}`tasks.json`. [List of tasks](https://zed.dev/docs/tasks) that can be run from the command palette. ''; }; userDebug = mkOption { inherit (jsonFormat) type; default = [ ]; example = [ { label = "Go (Delve)"; adapter = "Delve"; program = "$ZED_FILE"; request = "launch"; mode = "debug"; } ]; description = '' Configuration written to Zed's {file}`debug.json`. Global debug configurations for Zed's [Debugger](https://zed.dev/docs/debugger). ''; }; extensions = mkOption { type = types.listOf types.str; default = [ ]; example = [ "swift" "nix" "xy-zed" ]; description = '' A list of the extensions Zed should install on startup. Use the name of a repository in the [extension list](https://github.com/zed-industries/extensions/tree/main/extensions). ''; }; installRemoteServer = mkOption { type = types.bool; default = false; example = true; description = '' Whether to symlink the Zed's remote server binary to the expected location. This allows remotely connecting to this system from a distant Zed client. For more information, consult the ["Remote Server" section](https://wiki.nixos.org/wiki/Zed#Remote_Server) in the wiki. ''; }; enableMcpIntegration = mkOption { type = types.bool; default = false; description = '' Whether to integrate the MCP server config from {option}`programs.mcp.servers` into {option}`programs.zed-editor.userSettings.context_servers`. Note: Settings defined in {option}`programs.zed-editor.userSettings.context_servers` will take precedence over the generated MCP configuration. ''; }; themes = mkOption { description = '' Each theme is written to {file}`$XDG_CONFIG_HOME/zed/themes/theme-name.json` where the name of each attribute is the theme-name See for the structure of a Zed theme ''; type = types.attrsOf ( types.oneOf [ jsonFormat.type types.path types.lines ] ); default = { }; }; defaultEditor = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to set {command}`zeditor -w` as the default editor using the {env}`EDITOR` and {env}`VISUAL` environment variables. ''; }; }; }; config = mkIf cfg.enable { assertions = [ { assertion = cfg.extraPackages != [ ] -> cfg.package != null; message = "{option}programs.zed-editor.extraPackages requires non null {option}programs.zed-editor.package"; } { assertion = cfg.defaultEditor -> cfg.package != null; message = "{option}programs.zed-editor.defaultEditor requires non null {option}programs.zed-editor.package"; } ]; home.packages = mkIf (cfg.package != null) ( if cfg.extraPackages != [ ] then [ (pkgs.symlinkJoin { name = "${lib.getName cfg.package}-wrapped-${lib.getVersion cfg.package}"; paths = [ cfg.package ]; preferLocalBuild = true; nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/${cfg.package.meta.mainProgram or "zeditor"} \ --suffix PATH : ${lib.makeBinPath cfg.extraPackages} ''; }) ] else [ cfg.package ] ); home.file = mkIf (cfg.installRemoteServer && (cfg.package ? remote_server)) ( let inherit (cfg.package) remote_server; binaryName = cfg.package.remoteServerExecutableName; in { ".zed_server/${binaryName}".source = lib.getExe' remote_server binaryName; } ); home.activation = mkMerge [ (mkIf (immutableConfigCleanup != "") { zedImmutableConfig = lib.hm.dag.entryBetween [ "linkGeneration" ] [ "writeBoundary" ] immutableConfigCleanup; }) (mkIf (cfg.mutableUserSettings && mergedSettings != { }) { zedSettingsActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] ( lib.hm.generators.mkImpureConfigMerger { inherit pkgs; format = "json"; empty = "{}"; jqOperation = "$dynamic * $static"; path = "${config.xdg.configHome}/zed/settings.json"; staticSettings = jsonFormat.generate "zed-user-settings" mergedSettings; reader = "${lib.getExe json5} --as-json"; } ); }) (mkIf (cfg.mutableUserKeymaps && cfg.userKeymaps != [ ]) { zedKeymapActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] ( lib.hm.generators.mkImpureConfigMerger { inherit pkgs; format = "json"; empty = "[]"; jqOperation = "$dynamic + $static | group_by(.context) | map(reduce .[] as \$item ({}; . * \$item))"; path = "${config.xdg.configHome}/zed/keymap.json"; staticSettings = jsonFormat.generate "zed-user-keymaps" cfg.userKeymaps; reader = "${lib.getExe json5} --as-json"; } ); }) (mkIf (cfg.mutableUserTasks && cfg.userTasks != [ ]) { zedTasksActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] ( lib.hm.generators.mkImpureConfigMerger { inherit pkgs; format = "json"; empty = "[]"; jqOperation = "$dynamic + $static | group_by(.label) | map(reduce .[] as \$item ({}; . * \$item))"; path = "${config.xdg.configHome}/zed/tasks.json"; staticSettings = jsonFormat.generate "zed-user-tasks" cfg.userTasks; reader = "${lib.getExe json5} --as-json"; } ); }) (mkIf (cfg.mutableUserDebug && cfg.userDebug != [ ]) { zedDebugActivation = lib.hm.dag.entryAfter [ "linkGeneration" ] ( lib.hm.generators.mkImpureConfigMerger { inherit pkgs; format = "json"; empty = "[]"; jqOperation = "$dynamic + $static | group_by(.label) | map(reduce .[] as \$item ({}; . * \$item))"; path = "${config.xdg.configHome}/zed/debug.json"; staticSettings = jsonFormat.generate "zed-user-debug" cfg.userDebug; reader = "${lib.getExe json5} --as-json"; } ); }) ]; xdg.configFile = mkMerge [ (lib.mapAttrs' ( n: v: lib.nameValuePair "zed/themes/${n}.json" { source = if lib.isString v then pkgs.writeText "zed-theme-${n}" v else if builtins.isPath v || lib.isStorePath v then v else jsonFormat.generate "zed-theme-${n}" v; } ) cfg.themes) (mkIf immutableFiles.settings { "zed/settings.json".source = jsonFormat.generate "zed-user-settings" mergedSettings; }) (mkIf immutableFiles.keymap { "zed/keymap.json".source = jsonFormat.generate "zed-user-keymaps" cfg.userKeymaps; }) (mkIf immutableFiles.tasks { "zed/tasks.json".source = jsonFormat.generate "zed-user-tasks" cfg.userTasks; }) (mkIf immutableFiles.debug { "zed/debug.json".source = jsonFormat.generate "zed-user-debug" cfg.userDebug; }) ]; home.sessionVariables = mkIf cfg.defaultEditor editorEnv; }; }