diff --git a/README.md b/README.md index 067caacf..ec0ca4f0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,44 @@ This package implements a TypeScript language service plugin that allows additio "typescript.enablePromptUseWorkspaceTsdk": true } ``` + - In Zed, `effect-language-service setup` can add or update `.zed/settings.json` so `vtsls` loads workspace TypeScript plugins: + ```jsonc + { + "languages": { + "TSX": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + }, + "TypeScript": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + } + }, + "lsp": { + "vtsls": { + "settings": { + "typescript": { + "tsserver": { + "pluginPaths": [ + "./node_modules" + ] + } + }, + "vtsls": { + "autoUseWorkspaceTsdk": true + } + } + } + } + } + ``` + `show_edit_predictions` is intentionally not configured. - In JetBrains you may have to disable the Vue language service, and choose the workspace version of TypeScript in the settings from the dropdown. - In NVim with nvim-vtsls you should refer to [how to enable TypeScript plugins in vtsls](https://github.com/yioneko/vtsls?tab=readme-ov-file#typescript-plugin-not-activated) - In Emacs, additional steps are required to enable LSPs, [step by step instructions can be found here](https://gosha.net/2025/effect-ls-emacs/) diff --git a/packages/harness-effect-v3/__snapshots__/setup-cli.test.ts.snap b/packages/harness-effect-v3/__snapshots__/setup-cli.test.ts.snap index 1de6fe11..79e1023d 100644 --- a/packages/harness-effect-v3/__snapshots__/setup-cli.test.ts.snap +++ b/packages/harness-effect-v3/__snapshots__/setup-cli.test.ts.snap @@ -140,6 +140,83 @@ exports[`Setup CLI > should add LSP with VS Code editor selected and configure V }" `; +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > .zed/settings.json 1`] = ` +"{ + "languages": { + "TSX": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + }, + "TypeScript": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + } + }, + "lsp": { + "vtsls": { + "settings": { + "typescript": { + "tsserver": { + "pluginPaths": [ + "./node_modules" + ] + } + }, + "vtsls": { + "autoUseWorkspaceTsdk": true + } + } + } + } +} +" +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > change summary 1`] = ` +[ + { + "description": "Add @effect/language-service@workspace:* to devDependencies", + "file": "package.json", + }, + { + "description": "Add $schema to tsconfig; Add plugins array with @effect/language-service plugin", + "file": "tsconfig.json", + }, + { + "description": "Create .zed/settings.json with vtsls settings", + "file": ".zed/settings.json", + }, +] +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > package.json 1`] = ` +"{ + "name": "test-project", + "version": "1.0.0", + "dependencies": {}, +"devDependencies": { "@effect/language-service": "workspace:*" } +}" +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "strict": true, + "target": "ES2022", +"plugins": [ + { "name": "@effect/language-service" } +] + }, +"$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" +}" +`; + exports[`Setup CLI > should generate changes for adding LSP with custom diagnostic severities > change summary 1`] = ` [ { @@ -612,6 +689,93 @@ exports[`Setup CLI > should preserve existing VSCode settings when adding LSP-sp }" `; +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > .zed/settings.json 1`] = ` +"{ + "theme": "Ayu Dark", + "languages": { + "TSX": { + "formatter": "prettier", + "language_servers": [ + "vtsls", + "typescript-language-server", + "...", + "custom-tsx-server" + ] + }, + "JavaScript": { + "formatter": "prettier" + }, + "TypeScript": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + } + }, + "lsp": { + "vtsls": { + "initialization_options": { + "maxTsServerMemory": 4096 + }, + "settings": { + "vtsls": { + "experimental": true, + "autoUseWorkspaceTsdk": true + }, + "typescript": { + "tsserver": { + "pluginPaths": [ + "./node_modules" + ] + } + } + } + } + } +} +" +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > change summary 1`] = ` +[ + { + "description": "Add @effect/language-service@workspace:* to devDependencies", + "file": "package.json", + }, + { + "description": "Add $schema to tsconfig; Add plugins array with @effect/language-service plugin", + "file": "tsconfig.json", + }, + { + "description": "Update .zed/settings.json with vtsls settings", + "file": ".zed/settings.json", + }, +] +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > package.json 1`] = ` +"{ + "name": "test-project", + "version": "1.0.0", + "dependencies": {}, +"devDependencies": { "@effect/language-service": "workspace:*" } +}" +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "strict": true, + "target": "ES2022", +"plugins": [ + { "name": "@effect/language-service" } +] + }, +"$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" +}" +`; + exports[`Setup CLI > should remove Effect Language Service section from CLAUDE.md when uninstalling LSP > CLAUDE.md 1`] = ` "# CLAUDE.md diff --git a/packages/harness-effect-v4/__snapshots__/setup-cli.test.ts.snap b/packages/harness-effect-v4/__snapshots__/setup-cli.test.ts.snap index 1de6fe11..79e1023d 100644 --- a/packages/harness-effect-v4/__snapshots__/setup-cli.test.ts.snap +++ b/packages/harness-effect-v4/__snapshots__/setup-cli.test.ts.snap @@ -140,6 +140,83 @@ exports[`Setup CLI > should add LSP with VS Code editor selected and configure V }" `; +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > .zed/settings.json 1`] = ` +"{ + "languages": { + "TSX": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + }, + "TypeScript": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + } + }, + "lsp": { + "vtsls": { + "settings": { + "typescript": { + "tsserver": { + "pluginPaths": [ + "./node_modules" + ] + } + }, + "vtsls": { + "autoUseWorkspaceTsdk": true + } + } + } + } +} +" +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > change summary 1`] = ` +[ + { + "description": "Add @effect/language-service@workspace:* to devDependencies", + "file": "package.json", + }, + { + "description": "Add $schema to tsconfig; Add plugins array with @effect/language-service plugin", + "file": "tsconfig.json", + }, + { + "description": "Create .zed/settings.json with vtsls settings", + "file": ".zed/settings.json", + }, +] +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > package.json 1`] = ` +"{ + "name": "test-project", + "version": "1.0.0", + "dependencies": {}, +"devDependencies": { "@effect/language-service": "workspace:*" } +}" +`; + +exports[`Setup CLI > should add LSP with Zed editor selected and create Zed settings > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "strict": true, + "target": "ES2022", +"plugins": [ + { "name": "@effect/language-service" } +] + }, +"$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" +}" +`; + exports[`Setup CLI > should generate changes for adding LSP with custom diagnostic severities > change summary 1`] = ` [ { @@ -612,6 +689,93 @@ exports[`Setup CLI > should preserve existing VSCode settings when adding LSP-sp }" `; +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > .zed/settings.json 1`] = ` +"{ + "theme": "Ayu Dark", + "languages": { + "TSX": { + "formatter": "prettier", + "language_servers": [ + "vtsls", + "typescript-language-server", + "...", + "custom-tsx-server" + ] + }, + "JavaScript": { + "formatter": "prettier" + }, + "TypeScript": { + "language_servers": [ + "vtsls", + "typescript-language-server", + "..." + ] + } + }, + "lsp": { + "vtsls": { + "initialization_options": { + "maxTsServerMemory": 4096 + }, + "settings": { + "vtsls": { + "experimental": true, + "autoUseWorkspaceTsdk": true + }, + "typescript": { + "tsserver": { + "pluginPaths": [ + "./node_modules" + ] + } + } + } + } + } +} +" +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > change summary 1`] = ` +[ + { + "description": "Add @effect/language-service@workspace:* to devDependencies", + "file": "package.json", + }, + { + "description": "Add $schema to tsconfig; Add plugins array with @effect/language-service plugin", + "file": "tsconfig.json", + }, + { + "description": "Update .zed/settings.json with vtsls settings", + "file": ".zed/settings.json", + }, +] +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > package.json 1`] = ` +"{ + "name": "test-project", + "version": "1.0.0", + "dependencies": {}, +"devDependencies": { "@effect/language-service": "workspace:*" } +}" +`; + +exports[`Setup CLI > should preserve existing Zed settings when adding missing vtsls config > tsconfig.json 1`] = ` +"{ + "compilerOptions": { + "strict": true, + "target": "ES2022", +"plugins": [ + { "name": "@effect/language-service" } +] + }, +"$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" +}" +`; + exports[`Setup CLI > should remove Effect Language Service section from CLAUDE.md when uninstalling LSP > CLAUDE.md 1`] = ` "# CLAUDE.md diff --git a/packages/language-service/src/cli/setup/assessment.ts b/packages/language-service/src/cli/setup/assessment.ts index ba7d5123..2367dd7a 100644 --- a/packages/language-service/src/cli/setup/assessment.ts +++ b/packages/language-service/src/cli/setup/assessment.ts @@ -36,6 +36,7 @@ export namespace Assessment { readonly packageJson: FileInput // Required readonly tsconfig: FileInput // Required readonly vscodeSettings: Option.Option // Optional + readonly zedSettings: Option.Option // Optional readonly agentsMd: Option.Option // Optional - agents.md readonly claudeMd: Option.Option // Optional - CLAUDE.md } @@ -67,14 +68,24 @@ export namespace Assessment { } /** - * .vscode/settings.json assessment result + * JSON editor settings assessment result */ - export interface VSCodeSettings { + export interface EditorSettings { readonly path: string readonly sourceFile: ts.JsonSourceFile // AST for modification readonly settings: Record // Parsed JSON content } + /** + * .vscode/settings.json assessment result + */ + export interface VSCodeSettings extends EditorSettings {} + + /** + * .zed/settings.json assessment result + */ + export interface ZedSettings extends EditorSettings {} + /** * Markdown file assessment result (for CLAUDE.md or agents.md) */ @@ -92,6 +103,7 @@ export namespace Assessment { readonly packageJson: PackageJson // Required readonly tsconfig: TsConfig // Required readonly vscodeSettings: Option.Option // Optional + readonly zedSettings: Option.Option // Optional readonly agentsMd: Option.Option // Optional - agents.md readonly claudeMd: Option.Option // Optional - CLAUDE.md } @@ -190,11 +202,11 @@ const assessTsConfigFromInput = ( }) /** - * Assess VSCode settings from input + * Assess JSON editor settings from input */ -const assessVSCodeSettingsFromInput = ( +const assessEditorSettingsFromInput = ( input: FileInput -): Effect.Effect => +): Effect.Effect => Effect.gen(function*() { const ts = yield* TypeScriptContext @@ -264,19 +276,25 @@ export const createAssessmentInput = ( text: packageJsonText } - const vscodeSettingsPath = path.join(currentDir, ".vscode", "settings.json") - const vscodeSettingsExists = yield* fs.exists(vscodeSettingsPath) - - let vscodeSettingsInput = Option.none() - if (vscodeSettingsExists) { - const vscodeSettingsText = yield* fs.readFileString(vscodeSettingsPath).pipe( - Effect.mapError((cause) => new FileReadError({ path: vscodeSettingsPath, cause })) - ) - vscodeSettingsInput = Option.some({ - fileName: vscodeSettingsPath, - text: vscodeSettingsText + const readOptionalFileInput = (filePath: string) => + Effect.gen(function*() { + const exists = yield* fs.exists(filePath) + if (!exists) { + return Option.none() + } + + const text = yield* fs.readFileString(filePath).pipe( + Effect.mapError((cause) => new FileReadError({ path: filePath, cause })) + ) + + return Option.some({ + fileName: filePath, + text + }) }) - } + + const vscodeSettingsInput = yield* readOptionalFileInput(path.join(currentDir, ".vscode", "settings.json")) + const zedSettingsInput = yield* readOptionalFileInput(path.join(currentDir, ".zed", "settings.json")) const agentsMdLowerPath = path.join(currentDir, "agents.md") const agentsMdUpperPath = path.join(currentDir, "AGENTS.md") @@ -330,6 +348,7 @@ export const createAssessmentInput = ( packageJson: packageJsonInput, tsconfig: tsconfigInput, vscodeSettings: vscodeSettingsInput, + zedSettings: zedSettingsInput, agentsMd: agentsMdInput, claudeMd: claudeMdInput } @@ -350,9 +369,14 @@ export const assess = ( // Assess VSCode settings (optional) const vscodeSettings = Option.isSome(input.vscodeSettings) - ? Option.some(yield* assessVSCodeSettingsFromInput(input.vscodeSettings.value)) + ? Option.some(yield* assessEditorSettingsFromInput(input.vscodeSettings.value)) : Option.none() + // Assess Zed settings (optional) + const zedSettings = Option.isSome(input.zedSettings) + ? Option.some(yield* assessEditorSettingsFromInput(input.zedSettings.value)) + : Option.none() + // Assess markdown files (optional) const agentsMd = Option.isSome(input.agentsMd) ? Option.some(assessMarkdownFromInput(input.agentsMd.value)) @@ -366,6 +390,7 @@ export const assess = ( packageJson, tsconfig, vscodeSettings, + zedSettings, agentsMd, claudeMd } diff --git a/packages/language-service/src/cli/setup/changes.ts b/packages/language-service/src/cli/setup/changes.ts index 7062f9a6..328c34a1 100644 --- a/packages/language-service/src/cli/setup/changes.ts +++ b/packages/language-service/src/cli/setup/changes.ts @@ -19,6 +19,117 @@ interface ComputeFileChangesResult { } const TSCONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" +const ZED_SETTINGS_RELATIVE_PATH = ".zed/settings.json" +const ZED_LANGUAGE_SERVERS = ["vtsls", "typescript-language-server", "..."] as const +const ZED_PLUGIN_PATHS = ["./node_modules"] as const + +type JsonObject = Record + +const isJsonObject = (value: unknown): value is JsonObject => + typeof value === "object" && value !== null && !Array.isArray(value) + +const cloneJsonValue = (value: A): A => JSON.parse(JSON.stringify(value)) as A + +const formatJson = (value: JsonObject): string => `${JSON.stringify(value, null, 2)}\n` + +const createZedSettingsTarget = (): Target.ZedSettings => ({ + settings: { + languages: { + TSX: { + language_servers: [...ZED_LANGUAGE_SERVERS] + }, + TypeScript: { + language_servers: [...ZED_LANGUAGE_SERVERS] + } + }, + lsp: { + vtsls: { + settings: { + typescript: { + tsserver: { + pluginPaths: [...ZED_PLUGIN_PATHS] + } + }, + vtsls: { + autoUseWorkspaceTsdk: true + } + } + } + } + } +}) + +const mergeRequiredArrayValues = ( + current: unknown, + required: ReadonlyArray +): unknown => { + if (current === undefined) { + return cloneJsonValue(required) + } + + if (!Array.isArray(current)) { + return current + } + + const missingRequired = required.filter((requiredValue) => + !current.some((currentValue) => currentValue === requiredValue) + ) + + if (missingRequired.length === 0) { + return current + } + + return [ + ...required, + ...current.filter((currentValue) => !required.some((requiredValue) => currentValue === requiredValue)) + ] +} + +const mergeMissingJson = (current: unknown, target: unknown): unknown => { + if (Array.isArray(target)) { + return mergeRequiredArrayValues(current, target) + } + + if (isJsonObject(target)) { + if (current === undefined) { + return cloneJsonValue(target) + } + + if (!isJsonObject(current)) { + return current + } + + let changed = false + const next: JsonObject = { ...current } + + for (const [key, targetValue] of Object.entries(target)) { + const currentValue = next[key] + if (currentValue === undefined) { + next[key] = cloneJsonValue(targetValue) + changed = true + } else { + const mergedValue = mergeMissingJson(currentValue, targetValue) + if (mergedValue !== currentValue) { + next[key] = mergedValue + changed = true + } + } + } + + return changed ? next : current + } + + return current === undefined ? target : current +} + +const getProjectFilePath = (packageJsonPath: string, relativePath: string): string => { + const separatorIndex = Math.max(packageJsonPath.lastIndexOf("/"), packageJsonPath.lastIndexOf("\\")) + if (separatorIndex === -1) { + return relativePath + } + + return `${packageJsonPath.slice(0, separatorIndex + 1)}${relativePath}` +} /** * Create an empty ComputeFileChangesResult @@ -87,6 +198,16 @@ export const computeChanges = ( } } + // Compute Zed settings changes if user selected Zed editor + if (target.editors.includes("zed") && Option.isSome(target.packageJson.lspVersion)) { + const zedResult = yield* computeZedSettingsChanges( + assessment, + createZedSettingsTarget() + ) + codeActions = [...codeActions, ...zedResult.codeActions] + messages = [...messages, ...zedResult.messages] + } + // Compute markdown file changes if the files exist const shouldInstallLsp = Option.isSome(target.packageJson.lspVersion) @@ -132,6 +253,16 @@ export const computeChanges = ( ] } + if (target.editors.includes("zed")) { + messages = [ + ...messages, + "Zed:", + " The CLI added .zed/settings.json so Zed uses vtsls with workspace TypeScript plugins.", + " Reload the Zed language server or workspace if diagnostics do not appear.", + "" + ] + } + if (target.editors.includes("emacs")) { messages = [ ...messages, @@ -981,6 +1112,59 @@ const computeVSCodeSettingsChanges = ( }) } +/** + * Compute .zed/settings.json changes + */ +const computeZedSettingsChanges = ( + assessment: Assessment.State, + target: Target.ZedSettings +): Effect.Effect => { + const current = assessment.zedSettings + const targetSettings = target.settings + + if (Option.isNone(current)) { + const newText = formatJson(targetSettings) + return Effect.succeed({ + codeActions: [{ + description: "Create .zed/settings.json with vtsls settings", + changes: [{ + fileName: getProjectFilePath(assessment.packageJson.path, ZED_SETTINGS_RELATIVE_PATH), + textChanges: [{ + span: { start: 0, length: 0 }, + newText + }], + isNewFile: true + }] + }], + messages: [] + }) + } + + const nextSettings = mergeMissingJson(current.value.settings, targetSettings) as JsonObject + if (nextSettings === current.value.settings) { + return Effect.succeed(emptyFileChangesResult()) + } + + const newText = formatJson(nextSettings) + if (newText === current.value.sourceFile.text) { + return Effect.succeed(emptyFileChangesResult()) + } + + return Effect.succeed({ + codeActions: [{ + description: "Update .zed/settings.json with vtsls settings", + changes: [{ + fileName: current.value.path, + textChanges: [{ + span: { start: 0, length: current.value.sourceFile.text.length }, + newText + }] + }] + }], + messages: [] + }) +} + export const reviewAndApplyChanges = ( result: ComputeChangesResult, assessmentState: Assessment.State, diff --git a/packages/language-service/src/cli/setup/diff-renderer.ts b/packages/language-service/src/cli/setup/diff-renderer.ts index 757317d7..7aabb238 100644 --- a/packages/language-service/src/cli/setup/diff-renderer.ts +++ b/packages/language-service/src/cli/setup/diff-renderer.ts @@ -233,6 +233,17 @@ export function renderFileChanges( return lines } +/** + * Render a new file as added lines + */ +function renderNewFileChanges(textChanges: ReadonlyArray): ReadonlyArray { + const newText = textChanges.map((change) => change.newText).join("") + return newText + .split("\n") + .filter((line, index, lines) => !(index === lines.length - 1 && line.length === 0)) + .map((line) => renderLine(undefined, "+", line, GREEN)) +} + /** * Render code actions with diffs */ @@ -255,6 +266,9 @@ export const renderCodeActions = ( if (Option.isSome(assessmentState.vscodeSettings)) { sourceFiles.push(assessmentState.vscodeSettings.value.sourceFile) } + if (Option.isSome(assessmentState.zedSettings)) { + sourceFiles.push(assessmentState.zedSettings.value.sourceFile) + } // Collect plain text files from assessment state (markdown files) const plainTextFiles: Array<{ path: string; text: string }> = [] @@ -295,6 +309,10 @@ export const renderCodeActions = ( // Use plain text renderer for diff generation (markdown files) const diffLines = renderPlainTextFileChanges(plainTextFile.text, fileChange.textChanges) yield* Console.log(diffLines.join("\n")) + } else if (fileChange.isNewFile) { + // New files are not present in assessment state, so render the full content as additions. + const diffLines = renderNewFileChanges(fileChange.textChanges) + yield* Console.log(diffLines.join("\n")) } else { // File not in assessment state, just mention we want to change it yield* Console.log(" (file will be modified)") diff --git a/packages/language-service/src/cli/setup/target-prompt.ts b/packages/language-service/src/cli/setup/target-prompt.ts index 2e5e5976..adf64cce 100644 --- a/packages/language-service/src/cli/setup/target-prompt.ts +++ b/packages/language-service/src/cli/setup/target-prompt.ts @@ -63,6 +63,7 @@ export const gatherTargetState = ( diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } } @@ -133,8 +134,9 @@ export const gatherTargetState = ( }) // Editor Selection - Using multi-select - // Pre-select VSCode if .vscode/settings.json exists + // Pre-select editors when their settings files exist const hasVscodeSettings = Option.isSome(assessment.vscodeSettings) + const hasZedSettings = Option.isSome(assessment.zedSettings) const editors = yield* Prompt.multiSelect({ message: "Which editors do you use?", @@ -144,6 +146,11 @@ export const gatherTargetState = ( value: "vscode" as Editor, selected: hasVscodeSettings }, + { + title: "Zed", + value: "zed" as Editor, + selected: hasZedSettings + }, { title: "Neovim", value: "nvim" as Editor @@ -165,6 +172,7 @@ export const gatherTargetState = ( diagnosticSeverities }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors } }) diff --git a/packages/language-service/src/cli/setup/target.ts b/packages/language-service/src/cli/setup/target.ts index 155f34a3..f2b223bf 100644 --- a/packages/language-service/src/cli/setup/target.ts +++ b/packages/language-service/src/cli/setup/target.ts @@ -5,7 +5,7 @@ import type * as Assesment from "./assessment" /** * Supported editor types */ -export type Editor = "vscode" | "nvim" | "emacs" +export type Editor = "vscode" | "zed" | "nvim" | "emacs" /** * Target namespace containing all target configuration types @@ -36,6 +36,13 @@ export namespace Target { readonly settings: Record // Desired settings } + /** + * Target .zed/settings.json configuration + */ + export interface ZedSettings { + readonly settings: Record // Desired settings + } + /** * Complete target state defining what configuration should be achieved */ @@ -43,6 +50,7 @@ export namespace Target { readonly packageJson: PackageJson readonly tsconfig: TsConfig readonly vscodeSettings: Option.Option + readonly zedSettings: Option.Option readonly editors: ReadonlyArray } } @@ -60,6 +68,9 @@ export const fromAssessment = (inputState: Assesment.Assessment.State): Target.S vscodeSettings: Option.map(inputState.vscodeSettings, (settings) => ({ settings: settings.settings })), + zedSettings: Option.map(inputState.zedSettings, (settings) => ({ + settings: settings.settings + })), editors: [] }) diff --git a/packages/language-service/test/changes.test.ts b/packages/language-service/test/changes.test.ts index 663dbd1a..facc3fa7 100644 --- a/packages/language-service/test/changes.test.ts +++ b/packages/language-service/test/changes.test.ts @@ -17,6 +17,7 @@ function createAssessmentInput(tsconfig: Record): Assessment.In text: JSON.stringify(tsconfig, null, 2) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), agentsMd: Option.none(), claudeMd: Option.none() } @@ -52,6 +53,7 @@ describe("computeChanges", () => { diagnosticSeverities: Option.some({ floatingEffect: "warning" }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } diff --git a/packages/language-service/test/config-cli.test.ts b/packages/language-service/test/config-cli.test.ts index fe645d46..75e2a8ce 100644 --- a/packages/language-service/test/config-cli.test.ts +++ b/packages/language-service/test/config-cli.test.ts @@ -26,6 +26,7 @@ function createAssessmentInput( text: JSON.stringify(vscodeSettings, null, 2) }) : Option.none(), + zedSettings: Option.none(), agentsMd: Option.none(), claudeMd: Option.none() } @@ -75,6 +76,9 @@ describe("Config CLI", () => { expect(targetState.vscodeSettings).toEqual(Option.map(assessmentState.vscodeSettings, (settings) => ({ settings: settings.settings }))) + expect(targetState.zedSettings).toEqual(Option.map(assessmentState.zedSettings, (settings) => ({ + settings: settings.settings + }))) const result = await Effect.runPromise( computeChanges(assessmentState, targetState).pipe(Effect.provide(TypeScriptContext.live("."))) @@ -87,5 +91,8 @@ describe("Config CLI", () => { expect( result.codeActions.some((action) => action.changes.some((change) => change.fileName === ".vscode/settings.json")) ).toBe(false) + expect( + result.codeActions.some((action) => action.changes.some((change) => change.fileName === ".zed/settings.json")) + ).toBe(false) }) }) diff --git a/packages/language-service/test/setup-cli.test.ts b/packages/language-service/test/setup-cli.test.ts index e488a8aa..7bc211fc 100644 --- a/packages/language-service/test/setup-cli.test.ts +++ b/packages/language-service/test/setup-cli.test.ts @@ -14,7 +14,8 @@ function createTestAssessmentInput( tsconfig: Record, vscodeSettings?: Record, agentsMd?: string, - claudeMd?: string + claudeMd?: string, + zedSettings?: Record ): Assessment.Input { return { packageJson: { @@ -31,6 +32,12 @@ function createTestAssessmentInput( text: JSON.stringify(vscodeSettings, null, 2) }) : Option.none(), + zedSettings: zedSettings + ? Option.some({ + fileName: ".zed/settings.json", + text: JSON.stringify(zedSettings, null, 2) + }) + : Option.none(), agentsMd: agentsMd !== undefined ? Option.some({ fileName: "AGENTS.md", @@ -135,7 +142,28 @@ export async function expectSetupChanges( }).not.toThrow() } - // 5. Snapshot of final AGENTS.md + // 5. Snapshot of final .zed/settings.json and validate it's valid JSON + const zedSettingsFileChange = result.codeActions + .flatMap((action) => action.changes) + .find((fc) => fc.fileName === ".zed/settings.json") + if (zedSettingsFileChange) { + const finalZedSettings = Option.isSome(assessmentInput.zedSettings) + ? applyTextChanges( + assessmentInput.zedSettings.value.text, + zedSettingsFileChange.textChanges + ) + : zedSettingsFileChange.textChanges.map((change) => change.newText).join("") + + expect(finalZedSettings).toMatchSnapshot(".zed/settings.json") + expect(finalZedSettings).not.toContain("show_edit_predictions") + + // Assert that the final .zed/settings.json is valid JSON + expect(() => { + JSON.parse(finalZedSettings) + }).not.toThrow() + } + + // 6. Snapshot of final AGENTS.md const agentsMdFileChange = result.codeActions .flatMap((action) => action.changes) .find((fc) => fc.fileName === "AGENTS.md") @@ -147,7 +175,7 @@ export async function expectSetupChanges( expect(finalAgentsMd).toMatchSnapshot("AGENTS.md") } - // 6. Snapshot of final CLAUDE.md + // 7. Snapshot of final CLAUDE.md const claudeMdFileChange = result.codeActions .flatMap((action) => action.changes) .find((fc) => fc.fileName === "CLAUDE.md") @@ -185,6 +213,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -210,6 +239,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.some({ floatingEffect: "warning" }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -250,6 +280,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -281,6 +312,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -314,6 +346,7 @@ describe("Setup CLI", () => { }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -354,6 +387,7 @@ describe("Setup CLI", () => { }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -384,6 +418,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -425,6 +460,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -467,6 +503,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -510,6 +547,7 @@ describe("Setup CLI", () => { }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -545,6 +583,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -587,6 +626,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -626,6 +666,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -657,6 +698,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: ["vscode"] } @@ -699,6 +741,7 @@ describe("Setup CLI", () => { }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -734,6 +777,7 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: ["vscode"] } @@ -781,12 +825,179 @@ describe("Setup CLI", () => { diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: ["vscode"] } await expectSetupChanges(assessmentInput, targetState) }) + it("should add LSP with Zed editor selected and create Zed settings", async () => { + const assessmentInput = createTestAssessmentInput( + { + name: "test-project", + version: "1.0.0", + dependencies: {} + }, + { + compilerOptions: { + strict: true, + target: "ES2022" + } + } + ) + + const targetState: Target.State = { + packageJson: { + lspVersion: Option.some({ dependencyType: "devDependencies" as const, version: "workspace:*" }), + prepareScript: false + }, + tsconfig: { + diagnosticSeverities: Option.none() + }, + vscodeSettings: Option.none(), + zedSettings: Option.none(), + editors: ["zed"] + } + + await expectSetupChanges(assessmentInput, targetState) + }) + + it("should preserve existing Zed settings when adding missing vtsls config", async () => { + const assessmentInput = createTestAssessmentInput( + { + name: "test-project", + version: "1.0.0", + dependencies: {} + }, + { + compilerOptions: { + strict: true, + target: "ES2022" + } + }, + undefined, + undefined, + undefined, + { + theme: "Ayu Dark", + languages: { + TSX: { + formatter: "prettier", + language_servers: ["custom-tsx-server"] + }, + JavaScript: { + formatter: "prettier" + } + }, + lsp: { + vtsls: { + initialization_options: { + maxTsServerMemory: 4096 + }, + settings: { + vtsls: { + experimental: true + } + } + } + } + } + ) + + const targetState: Target.State = { + packageJson: { + lspVersion: Option.some({ dependencyType: "devDependencies" as const, version: "workspace:*" }), + prepareScript: false + }, + tsconfig: { + diagnosticSeverities: Option.none() + }, + vscodeSettings: Option.none(), + zedSettings: Option.none(), + editors: ["zed"] + } + + await expectSetupChanges(assessmentInput, targetState) + }) + + it("should not overwrite existing Zed vtsls settings", async () => { + const schemaUrl = "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json" + const assessmentInput = createTestAssessmentInput( + { + name: "test-project", + version: "1.0.0", + devDependencies: { + "@effect/language-service": "workspace:*" + } + }, + { + $schema: schemaUrl, + compilerOptions: { + strict: true, + target: "ES2022", + plugins: [ + { + name: "@effect/language-service" + } + ] + } + }, + undefined, + undefined, + undefined, + { + languages: { + TSX: { + language_servers: ["vtsls", "typescript-language-server", "..."] + }, + TypeScript: { + language_servers: ["vtsls", "typescript-language-server", "..."] + } + }, + lsp: { + vtsls: { + settings: { + typescript: { + tsserver: { + pluginPaths: ["./node_modules", "./custom-plugin-dir"] + } + }, + vtsls: { + autoUseWorkspaceTsdk: false + } + } + } + } + } + ) + + const assessmentState = await Effect.runPromise( + assess(assessmentInput).pipe(Effect.provide(TypeScriptContext.live("."))) + ) + + const targetState: Target.State = { + packageJson: { + lspVersion: Option.some({ dependencyType: "devDependencies" as const, version: "workspace:*" }), + prepareScript: false + }, + tsconfig: { + diagnosticSeverities: Option.none() + }, + vscodeSettings: Option.none(), + zedSettings: Option.none(), + editors: ["zed"] + } + + const result = await Effect.runPromise( + computeChanges(assessmentState, targetState).pipe(Effect.provide(TypeScriptContext.live("."))) + ) + + expect( + result.codeActions.some((action) => action.changes.some((change) => change.fileName === ".zed/settings.json")) + ).toBe(false) + }) + it("should handle tsconfig with existing plugin having custom options and diagnosticSeverity", async () => { const assessmentInput = createTestAssessmentInput( { @@ -852,6 +1063,7 @@ describe("Setup CLI", () => { }) }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -893,6 +1105,7 @@ This is a TypeScript project using Effect. diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -940,6 +1153,7 @@ This is a TypeScript project using Effect. diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] } @@ -995,6 +1209,7 @@ This is a TypeScript project using Effect. diagnosticSeverities: Option.none() }, vscodeSettings: Option.none(), + zedSettings: Option.none(), editors: [] }