|
|
@@ -3,92 +3,102 @@ import os from "node:os"
|
|
|
import path from "node:path"
|
|
|
import { onCleanup, onMount } from "solid-js"
|
|
|
import { createStore } from "solid-js/store"
|
|
|
-import z from "zod"
|
|
|
+import { Option, Schema, SchemaGetter } from "effect"
|
|
|
import { isRecord } from "@/util/record"
|
|
|
import { createSimpleContext } from "./helper"
|
|
|
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
|
|
|
|
|
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
|
|
|
|
-const JsonRpcMessageSchema = z.object({
|
|
|
- id: z.union([z.number(), z.string(), z.null()]).optional(),
|
|
|
- method: z.string().optional(),
|
|
|
- params: z.unknown().optional(),
|
|
|
- result: z.unknown().optional(),
|
|
|
- error: z
|
|
|
- .object({
|
|
|
- code: z.number().optional(),
|
|
|
- message: z.string().optional(),
|
|
|
- })
|
|
|
- .optional(),
|
|
|
+const JsonRpcMessageSchema = Schema.Struct({
|
|
|
+ id: Schema.optional(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
|
|
|
+ method: Schema.optional(Schema.String),
|
|
|
+ params: Schema.optional(Schema.Unknown),
|
|
|
+ result: Schema.optional(Schema.Unknown),
|
|
|
+ error: Schema.optional(
|
|
|
+ Schema.Struct({
|
|
|
+ code: Schema.optional(Schema.Number),
|
|
|
+ message: Schema.optional(Schema.String),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
})
|
|
|
|
|
|
-const PositionSchema = z.object({
|
|
|
- line: z.number(),
|
|
|
- character: z.number(),
|
|
|
+const PositionSchema = Schema.Struct({
|
|
|
+ line: Schema.Number,
|
|
|
+ character: Schema.Number,
|
|
|
})
|
|
|
|
|
|
-const EditorSelectionRangeSchema = z.object({
|
|
|
- text: z.string(),
|
|
|
- selection: z.object({
|
|
|
+const EditorSelectionRangeSchema = Schema.Struct({
|
|
|
+ text: Schema.String,
|
|
|
+ selection: Schema.Struct({
|
|
|
start: PositionSchema,
|
|
|
end: PositionSchema,
|
|
|
}),
|
|
|
})
|
|
|
|
|
|
-const EditorSelectionSchema = z
|
|
|
- .union([
|
|
|
- z.object({
|
|
|
- filePath: z.string(),
|
|
|
- source: z.enum(["websocket", "zed"]).optional(),
|
|
|
- ranges: z.array(EditorSelectionRangeSchema).min(1),
|
|
|
- }),
|
|
|
- z.object({
|
|
|
- text: z.string(),
|
|
|
- filePath: z.string(),
|
|
|
- source: z.enum(["websocket", "zed"]).optional(),
|
|
|
- selection: z.object({
|
|
|
- start: PositionSchema,
|
|
|
- end: PositionSchema,
|
|
|
- }),
|
|
|
+const EditorSelectionRangesSchema = Schema.Struct({
|
|
|
+ filePath: Schema.String,
|
|
|
+ source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
|
|
+ ranges: Schema.mutable(Schema.Array(EditorSelectionRangeSchema).check(Schema.isMinLength(1))),
|
|
|
+})
|
|
|
+
|
|
|
+const EditorSelectionSchema = Schema.Union([
|
|
|
+ EditorSelectionRangesSchema,
|
|
|
+ Schema.Struct({
|
|
|
+ text: Schema.String,
|
|
|
+ filePath: Schema.String,
|
|
|
+ source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
|
|
+ selection: Schema.Struct({
|
|
|
+ start: PositionSchema,
|
|
|
+ end: PositionSchema,
|
|
|
}),
|
|
|
- ])
|
|
|
- .transform((value) =>
|
|
|
- "ranges" in value
|
|
|
- ? value
|
|
|
- : {
|
|
|
- filePath: value.filePath,
|
|
|
- source: value.source,
|
|
|
- ranges: [
|
|
|
- {
|
|
|
- text: value.text,
|
|
|
- selection: value.selection,
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- )
|
|
|
-
|
|
|
-const EditorMentionSchema = z.object({
|
|
|
- filePath: z.string(),
|
|
|
- lineStart: z.number(),
|
|
|
- lineEnd: z.number(),
|
|
|
+ }),
|
|
|
+]).pipe(
|
|
|
+ Schema.decodeTo(EditorSelectionRangesSchema, {
|
|
|
+ decode: SchemaGetter.transform((value) =>
|
|
|
+ "ranges" in value
|
|
|
+ ? value
|
|
|
+ : {
|
|
|
+ filePath: value.filePath,
|
|
|
+ source: value.source,
|
|
|
+ ranges: [
|
|
|
+ {
|
|
|
+ text: value.text,
|
|
|
+ selection: value.selection,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ encode: SchemaGetter.passthrough({ strict: false }),
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
+const EditorMentionSchema = Schema.Struct({
|
|
|
+ filePath: Schema.String,
|
|
|
+ lineStart: Schema.Number,
|
|
|
+ lineEnd: Schema.Number,
|
|
|
})
|
|
|
|
|
|
-const EditorServerInfoSchema = z.object({
|
|
|
- protocolVersion: z.string().optional(),
|
|
|
- serverInfo: z
|
|
|
- .object({
|
|
|
- name: z.string().optional(),
|
|
|
- version: z.string().optional(),
|
|
|
- })
|
|
|
- .optional(),
|
|
|
+const EditorServerInfoSchema = Schema.Struct({
|
|
|
+ protocolVersion: Schema.optional(Schema.String),
|
|
|
+ serverInfo: Schema.optional(
|
|
|
+ Schema.Struct({
|
|
|
+ name: Schema.optional(Schema.String),
|
|
|
+ version: Schema.optional(Schema.String),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
})
|
|
|
|
|
|
-type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
|
|
-export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
|
|
-export type EditorMention = z.infer<typeof EditorMentionSchema>
|
|
|
+const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
|
|
|
+const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
|
|
|
+const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
|
|
|
+const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
|
|
|
+
|
|
|
+type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
|
|
|
+export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
|
|
|
+export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
|
|
|
export type EditorLabelState = "pending" | "sent" | "none"
|
|
|
-type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
|
|
+type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
|
|
|
|
|
|
type EditorConnection = {
|
|
|
url: string
|
|
|
@@ -214,16 +224,15 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|
|
const message = parseMessage(event.data)
|
|
|
if (!message) return
|
|
|
|
|
|
- const selection =
|
|
|
- message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
|
|
- if (selection?.success) {
|
|
|
- setSelection({ ...selection.data, source: "websocket" })
|
|
|
+ const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
|
|
|
+ if (Option.isSome(selection)) {
|
|
|
+ setSelection({ ...selection.value, source: "websocket" })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
|
|
- if (mention?.success) {
|
|
|
- mentionListeners.forEach((listener) => listener(mention.data))
|
|
|
+ const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
|
|
|
+ if (Option.isSome(mention)) {
|
|
|
+ mentionListeners.forEach((listener) => listener(mention.value))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -235,9 +244,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
|
|
pending.delete(message.id)
|
|
|
if (message.error) return
|
|
|
|
|
|
- const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
|
|
- if (initialize?.success) {
|
|
|
- setStore("server", initialize.data)
|
|
|
+ const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
|
|
|
+ if (Option.isSome(initialize)) {
|
|
|
+ setStore("server", initialize.value)
|
|
|
send({ method: "notifications/initialized" })
|
|
|
return
|
|
|
}
|
|
|
@@ -447,7 +456,7 @@ function parseMessage(value: unknown) {
|
|
|
if (typeof value !== "string") return
|
|
|
|
|
|
try {
|
|
|
- return JsonRpcMessageSchema.parse(JSON.parse(value))
|
|
|
+ return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
|
|
|
} catch {
|
|
|
return
|
|
|
}
|