目录

尝试用用看Zed

平常写的代码绝大多数都是Python、Kotlin、Java、Typescript之类的,也经常用Markdown写写文档。最近我还打算折腾折腾Rust。
这几天里,我在开始尝试用Zed来写这些代码。

由于自己的电脑内存比较小(16G在当今应该不算大吧),我最近看自己的IDEA和PyCharm尤为不爽。
虽然我不得不承认,macOS的虚拟内存这一块做的确实是不错,但是这也遮不住开的东西多了以后内存占用居高不下的事实,本子摸起来多少还是烫手。马上夏天就要来了,这属实是给我送来了一波温暖(

并且我自己的话,正如我前面几篇诸如「写在2026年刚开始的时候」在内的posts说的那样(话说我为什么连续两篇posts都在执着于引用这篇年终总结不过仔细想想也没办法因为总共也没几篇posts),从2025年初开始,AI属实是改变了我很多很多。
如今的话,我的很多代码都是借助AI完成的。我打开文本编辑工具要做的最大的工作已经从「所有代码都由我亲力亲为」变为了「审阅AI的代码,自己做一些项目各个模块的基建和设计为主,自己写代码为辅」的模式了。
实话实说,在这种变化下,绝大多数代码其实并不是我自己写的。我此时此刻更需要的是「如何舒服的看代码」,而不是「如何舒服的什么代码都要自己写」。

我相信这也是现在大多数开发的现状。如果要是在AI时代下什么代码都要自己写的话,那属实是「古法编程」了。
俗话说得好,由奢入俭难。试过AI coding以后,我觉得大多数人应该不会再去all in 古法编程了,毕竟有好的东西谁还不用呢。但也正是这种变化,我们似乎对传统的古法编程里的各种辅助功能的要求变得小了很多。

因此,我可能对IDEA这种重的IDE的需求变得不再那么大了。
目前的话我可能只有在有需要的时候才会打开IDEA、PyCharm这种重的IDE了。

那为啥不用VSCode呢?

首先当然是为了折腾,生命不息折腾不止(逃

我之前其实多次尝试过用VSCode来进行Java开发的工作。我不得不承认,VSCode有自己完善的插件生态,要是浅浅搞搞基本的开发工作的话,VSCode也是能搞的。当时VSCode下对Kotlin对支持是相当的垃圾。后续的话Kotlin官方出了kotlin-lsp,并且原生提供了对VSCode的插件支持,好了很多。

但是VSCode的资源占用其实也挺大的。
如果想让VSCode变得好用的话,那么VSCode肯定是要装一大堆插件的。我为了在VSCode里能方便用Java,装的插件有一堆:

  • Java:这是Oracle官方提供的Java插件
  • Language Support for Java(TM) by Red Hat:红帽提供的Java插件
  • Gradle for Java:微软给的插件,我写的项目用的Gradle
  • Debugger for Java:也是微软给的插件,调试Java代码用的
  • Project Manager for Java:仍然是微软给的插件,Java项目管理用的插件
  • Maven for Java:我也有Maven的项目,装个Maven的插件
  • Spring Initializr Java Support:搞Java的人多少都得搞搞Spring吧,Spring的Init用这个好用

可以看到Oracle、RedHat、Microsoft三家都给了对Java生态比较好用的插件。
但是有一说一啊,其实有很多功能各家给的是重叠的。比如RedHat的Java Support插件里其实囊括了Gradle for JavaMaven for Java这俩微软给的插件的,但是我总感觉支持其实不如微软给的。但是红帽的这个插件我也舍不得卸,因为我觉得这些插件其他的那些功能都还不错。

不过这也并不是说VSCode里提供这些插件就很“冗杂”。
首先,正如我前面所说,可能有了AI Agents以后,我绝大多数工作都是Code Review了,然后少量写一些代码。我可能对开发功能的健壮性不是那么看重了,轻量化地提供一些基础的功能就可以了。
其次,VSCode里装一些必要的插件,不装那么多插件也能实现目的,但是这样的话正如你所见,基本功能分散在各个插件里,每家都有各自的好,我还真很难说Java生态下哪个插件夯哪个插件拉。

由于我自己主要是搞Java的。我的看法是这样:如果要是想仔细琢磨琢磨Java的开发问题,那我不如打开我的IDEA;如果要是说想追求轻量化的话,我觉得也可以考虑考虑Zed呀。

另外我看网上的吹皮文章,说Zed性能快、没内置现代3A大作Chromium啊之类的balabala一堆优点。既然这么多人夸,那我觉得这玩意儿肯定多少有他自己的可取之处的。

但是这也不代表我不用VSCode,转而all in Zed了。该用我还是用,比如我用VSCode配合LaTeX Workshop插件写latex的文件。

Zed在2021年就出了,其实一开始是不咋喜欢Zed的。
当时我还是个Ubuntu用户。而Zed如果我没记错的话,当时只给macOS做了适配,还是在内测中的,不支持我用的Windows和Ubuntu。

Zed是用Rust做的。而当时我对Rust的看法,emmmmmmm还不是那么好。
当时正是Rust改造一切的时代,Rustlovers不厌其烦的跟你讲述「Rust Rewrite Everything」的传说。
我当时在群里聊天的时候,我脑子里突然蹦出来一个想法,我在群里说「Rust就像是编程届的原神」。结果没几周,我发现这个说法已经席卷互联网行业了。我觉得这个说法相比并不是因为我而得到广泛传播的,肯定是大家都不约而同自己内心深处迸发了这个想法,都觉得部分Rustlovers精神状态有待商榷导致的。
不过现在大家都看到Rust的影响力了。连Linux内核都已经认可了Rust了。我自己也打算尝试把Rust用起来了,我觉得真没啥必要无脑地坚定给Rust说No了。

回到Zed本身吧。Zed的发展是真的快。当时2021年的时候Zed还只能说是个概念产品,2023年到2024年还在快速迭代期,你说这玩意儿当主力,我真觉得它最多就是个txt文本编辑器。
但是2025年开始,我只能说士别三日即当刮目相待了。Zed显然已经成为了一个相当好用的编辑器了。并且这玩意儿更新频率还非常快,丝毫都没有要寄的样子。

而现在的话,Zed刚刚发布了1.0正式版,显然是个重大的里程碑。Zed越来越完善了。

我突发奇想,我在想Zed这个编辑器,对于搞Rust的这帮人来说,是否是受欢迎的呢?

然后我搜了搜,这玩意儿还真有现成的统计数据。在「Rust官方公布的2025年度调查报告结果」里,不难看出,2025年的Zed是Rust开发者圈子里占比第三的开发编辑器:

Which editor or IDE setup do you use with Rust code on a regular bas

可以说Zed还是不能小看的,短短一年时间发展还是非常迅猛的。

同时我也变得放心了很多,因为这帮Rewrite Everything的家伙们都在用,那这玩意儿相比不会太灵车(

不过既然说到这张图了,我觉得有必要吐槽一下这张图。
首先排名第一的是VSCode,这没啥好说的,VSCode是文本编辑器圈子里的神,而第二名vi/vim/neovim的话,只能说VIM用户不出意外的话应该是每个语言圈里的top3(除了易语言这种吧),这玩意儿上限非常高,无限自定义,这也没啥好说的。
这个榜单让我有认识到了Helix这个编辑器,我看这也是个终端里的编辑器。后面找个机会可能会考虑折腾折腾试试看,看着好像还不错(
我最纳闷的就是,这个倒数第一的Atom用户们,你们是在干啥???我去,现在还有人用Atom啊,感觉你们像是现代清朝老兵。
另外还有——Xcode用户们!哇!你们真是辛苦了(

总而言之,Zed应该是一个值得一用的文本编辑器。

我其实这里本来写了很多内容。但是我仔细一想,我没必要写一份官方Doc里一模一样的说明文档step-by-step做演示啊。

Zed整体是完完全全开箱即用的。我就用的是默认的One Dark配置,同时内置了大量的extensions。

我觉得对于我而言,唯一需要注意的就是:

  • 首先改改文本的大小,我不明白是我遇到啥bug了,我打开Zed以后默认是32这么大,还是得按Command+,或者右上角打开设置,搜索相关的配置改改
  • 终端字体也得改改
  • 安装对应的插件

Zed内置的AI功能也是一大亮点。
我自己的话订阅了Kimi带会员,我装了Kimi CLI,而Zed是ACP协议的提出者,这一块的支持非常好。
Kimi的话,可以轻轻松松给Kimi CLI添加External Agents,只需要点击加号添加一下就行了。
而Zed各个地方,包括Inline Assist之类的功能就不能调用Kimi Code了。并且Kimi官方也不给支持,这里要么就是订阅Zed官方的Plans,要么就是自己添加一个LLM Provider。
我自己的话是选择添加了DeepSeek,也很简单,我只需要在Deepseek官网生成一个API key,他直接就能输入进去完成配置,体验相当好。

完成这些后,Zed本体应该就没什么需要配置的了。其他内容完全可以按照自己的喜好和要求进行专门的配置,我觉得Zed官方的文档做的还是挺好的。

安装了官方的Java插件后,需要在Zed的settings.json里额外添加这样的配置,指定一下JDK地址和反编译器:

// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p)
{
  // 其他配置略...
  "lsp": {
    "jdtls": {
      "settings": {
        "java_home": "/opt/homebrew/opt/openjdk@25/",
        "lombok_support": true,
        "java": {
          "contentProvider": {
            "preferred": "fernflower",
          },
        },
      },
    },
  },
}

这里我改成了我自己本机上的一个JDK,然后我用fernflower来反编译。

这位才是我用Zed路上的绊脚石,我是真的服气了。这个得好好配置。

我们安装官方的kotlin插件,可以发现这个插件截止到我写这个post的4月30号,上次更新还是3月中旬,并且还是一些简单的bump。

安装完后,你打开一个kotlin项目调用kotlin-lsp,迎面而来就是第一个问题:

Language server kotlin-lsp:

from extension "Kotlin" version 0.2.1: download failed with status 404 Not Found

正如这个Issue所说,我们需要自己配置一下kotlin-lsp。

解决方式是去下载Jetbrains提供的kotlin-lsp,在官方的仓库里的Releases里下载最新版本(我下了v262.4739.0),下载Standalone Kotlin LSP Archive版本,解压到一个地方。

我这里放在了~/kotlin-lsp目录下,目录结构:

ls -l ~/kotlin-lsp

total 32
drwxr-xr-x@   3 tdiant  staff     96 Apr 30 04:51 jre
-rwxr-xr-x@   1 tdiant  staff   7707 Apr 30 23:57 kotlin-lsp-proxy.py
-rw-r--r--@   1 tdiant  staff   3405 Nov 30  1979 kotlin-lsp.cmd
-rwxr-xr-x@   1 tdiant  staff   4093 Nov 30  1979 kotlin-lsp.sh
drwxr-xr-x@ 645 tdiant  staff  20640 Apr 30 04:51 lib
drwxr-xr-x@   4 tdiant  staff    128 Apr 30 04:51 native

然后增加设置,手动配置上kotlin-lsp,在settings.json里加上这些配置:

// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p)
{
  // 其他配置略...
  "languages": {
    "Kotlin": {
      "language_servers": ["kotlin-lsp"],
    },
  },
  "lsp": {
    "kotlin-lsp": {
      "binary": { // 这里其实后面还要改
        "path": "/Users/tdiant/kotlin-lsp/kotlin-lsp.sh",
        "arguments": ["--stdio"],
      },
    },
    "kotlin-language-server": {
      "binary": {
        "env": {
          "JAVA_HOME": "/opt/homebrew/opt/openjdk@25/",
        },
      },
    },
}

如果你真按官方说的这样用了,那么在实际使用里会遇到两个对于我而言非常影响使用的问题:

  • 你右键看Show Definition后,如果你看的是kotlin支持库自带的类或者JDK标准库里的类,那没办法跳转到对应的文件里去,你跳转了也打开的是完全空白的文件,现象大概是跟这个老哥遇到的很类似
  • 你用补全功能会看到右下角报错Error: applying post-completion command,症状跟这个Issue相同

那咋办呢?

实际上这俩问题的根源来自于Zed本身和kotlin的LSP的支持问题。

前者是因为kotlin-lsp其实正确处理了这种情况,如果对应的文件有source,他会返回来一个类似/.../kotlin-stdlib-sources.jar!/jvmMain/kotlin/String.kt的地址,Zed是没办法解析xxx.jar!/path这样的zip包内地址的,所以他压根打不开。
但是实际上这个问题java插件那边是投机取巧绕过了的,解决方式是把这个文件复制到临时目录里面去,打开那个临时目录里的文件就行了。
所以这一点可以效仿这个做法来解决。

第二个问题是由于kotlin-lsp用到了window/showDocument这个LSP请求,而Zed这么多年过去了,至今都没有做这个支持!!!甚至有俩人发了俩PR(PR#25007PR#32239,全被关了,第二个PR还很离谱,官方折腾半天说「冲突太多+太忙了所以关了」直接给拒了。
这事Go那边也苦不堪言,在文档里也说了Zed的这种不作为
所以这个问题在有生之年,我感觉Zed怕是解决不了了。唯一的办法估计就是把kotlin-lsp的这个showDocument请求给他扔了。

而又由于Zed要考虑到他是基于WASM的,刚才那俩issue你要是让zed-extensions/kotlin来处理的话,其实他们压根就没法处理。
而我们要用的话,只能勉为其难从kotlin-lsp这边想想咋办了。

为此我和AI齐心合力折腾半天,写了一段Python脚本,保存在了~/Projects/kotlin-lsp/kotlin-lsp-proxy.py里了:

#!/usr/bin/env python3
import json
import os
import signal
import subprocess
import sys
import tempfile
import threading
import zipfile
from pathlib import Path

SOURCE_CACHE_DIR = Path(
    os.environ.get("KOTLIN_LSP_SOURCE_CACHE")
    or os.path.join(tempfile.gettempdir(), "kotlin-lsp-sources")
)
ZIP_LOCK = threading.Lock()


def read_message(f):
    headers = {}
    while True:
        line = f.readline()
        if not line:
            return None
        line = line.decode("utf-8").strip()
        if line == "":
            break
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        headers[key.strip().lower()] = value.strip()

    if "content-length" not in headers:
        return None

    length = int(headers["content-length"])
    body = b""
    while len(body) < length:
        chunk = f.read(length - len(body))
        if not chunk:
            return None
        body += chunk
    return body.decode("utf-8")


def write_message(f, payload_bytes):
    header = f"Content-Length: {len(payload_bytes)}\r\n\r\n".encode("utf-8")
    f.write(header + payload_bytes)
    f.flush()


def resolve_zip_uri(uri: str) -> str:
    if not uri or not isinstance(uri, str):
        return uri

    original = uri

    if uri.startswith("jar:"):
        uri = uri[4:]
    if uri.startswith("file://"):
        uri = uri[7:]
    elif uri.startswith("file:"):
        uri = uri[5:]

    lower = uri.lower()
    for ext in (".zip", ".jar"):
        idx = lower.rfind(ext + "!/")
        if idx == -1:
            continue

        zip_path = uri[: idx + len(ext)]
        internal_path = uri[idx + len(ext) + 2 :]

        if not os.path.isabs(zip_path):
            return original
        if not os.path.isfile(zip_path):
            return original

        cache_root = SOURCE_CACHE_DIR / Path(zip_path).name
        cache_file = cache_root / internal_path

        if cache_file.exists():
            return "file://" + str(cache_file)

        with ZIP_LOCK:
            if cache_file.exists():
                return "file://" + str(cache_file)

            try:
                with zipfile.ZipFile(zip_path, "r") as zf:
                    names = set(zf.namelist())
                    candidates = [
                        internal_path,
                        internal_path.lstrip("/"),
                        "/" + internal_path,
                    ]
                    target = None
                    for cand in candidates:
                        if cand in names:
                            target = cand
                            break

                    if target is None:
                        return original

                    cache_root.mkdir(parents=True, exist_ok=True)
                    zf.extract(target, cache_root)

                    extracted = cache_root / target
                    if extracted.exists():
                        return "file://" + str(extracted)
                    if cache_file.exists():
                        return "file://" + str(cache_file)
                    return original
            except Exception:
                return original

    return original


def replace_zip_uris(obj):
    if isinstance(obj, dict):
        for key, value in obj.items():
            if key in ("uri", "targetUri") and isinstance(value, str):
                obj[key] = resolve_zip_uri(value)
            else:
                replace_zip_uris(value)
    elif isinstance(obj, list):
        for item in obj:
            replace_zip_uris(item)


def forward_client_to_server(client_in, server_out, lock):
    try:
        while True:
            msg = read_message(client_in)
            if msg is None:
                break
            payload = msg.encode("utf-8")
            with lock:
                write_message(server_out, payload)
    except Exception as e:
        print(f"[proxy] client->server error: {e}", file=sys.stderr, flush=True)


def forward_server_to_client(server_in, client_out, server_out, lock):
    try:
        while True:
            msg = read_message(server_in)
            if msg is None:
                break

            try:
                data = json.loads(msg)

                if data.get("method") == "window/showDocument":
                    msg_id = data.get("id")
                    if msg_id is not None:
                        response = {
                            "jsonrpc": "2.0",
                            "id": msg_id,
                            "result": {"success": True},
                        }
                        response_bytes = json.dumps(
                            response, separators=(",", ":")
                        ).encode("utf-8")
                        with lock:
                            write_message(server_out, response_bytes)
                    continue

                replace_zip_uris(data)
                msg = json.dumps(data, separators=(",", ":"))

            except (json.JSONDecodeError, KeyError):
                pass

            payload = msg.encode("utf-8")
            write_message(client_out, payload)
    except Exception as e:
        print(f"[proxy] server->client error: {e}", file=sys.stderr, flush=True)


def forward_stderr(server_err):
    try:
        while True:
            line = server_err.readline()
            if not line:
                break
            sys.stderr.buffer.write(b"[lsp-stderr] " + line)
            sys.stderr.buffer.flush()
    except Exception as e:
        print(f"[proxy] stderr error: {e}", file=sys.stderr, flush=True)


def main():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    lsp_script = os.path.join(script_dir, "kotlin-lsp.sh")
    cache_dir = os.path.expanduser("~/.cache/kotlin-lsp")
    os.makedirs(cache_dir, exist_ok=True)
    os.makedirs(SOURCE_CACHE_DIR, exist_ok=True)

    proc = subprocess.Popen(
        [lsp_script, "--stdio", "--system-path", cache_dir],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        cwd=script_dir,
        bufsize=0,
    )

    # Ensure Java child is killed when proxy exits (SIGTERM, SIGINT, etc.)
    def cleanup(signum, frame):
        print(f"[proxy] caught signal {signum}, terminating child...", file=sys.stderr, flush=True)
        try:
            proc.terminate()
            proc.wait(timeout=5)
        except Exception:
            proc.kill()
        sys.exit(0)

    signal.signal(signal.SIGTERM, cleanup)
    signal.signal(signal.SIGINT, cleanup)

    server_lock = threading.Lock()

    t_client = threading.Thread(
        target=forward_client_to_server,
        args=(sys.stdin.buffer, proc.stdin, server_lock),
        daemon=True,
    )
    t_server = threading.Thread(
        target=forward_server_to_client,
        args=(proc.stdout, sys.stdout.buffer, proc.stdin, server_lock),
        daemon=True,
    )
    t_stderr = threading.Thread(
        target=forward_stderr,
        args=(proc.stderr,),
        daemon=True,
    )

    t_client.start()
    t_server.start()
    t_stderr.start()

    t_client.join()
    t_server.join()
    t_stderr.join()

    print("[proxy] stdin closed, shutting down child...", file=sys.stderr, flush=True)
    try:
        proc.terminate()
        proc.wait(timeout=10)
    except Exception:
        proc.kill()
        proc.wait()


if __name__ == "__main__":
    main()

然后在kotlin插件的配置里,把入口改了:

{
  // ...
  "lsp": {
    "kotlin-lsp": {
      "binary": {
        // 把下面的这些刚才做的配置:
        // "path": "/Users/tdiant/kotlin-lsp/kotlin-lsp.sh",
        // "arguments": ["--stdio"],
        // 入口改成我们刚才的这个python脚本:
        "path": "/Users/tdiant/kotlin-lsp/kotlin-lsp-proxy.py",
        "arguments": [],
      },
    },
  }
  //...
}

就基本够用了。现在kotlin终于是能看了,属实是非常费劲。

我目前的话其实已经用Zed一周了。用着用着恰好Zed出了1.0正式版,有感而发写了这篇post。

其实Zed还有一些可说的内容,我推荐阅读一下Laplace的这篇博文。Zed的配置非常不错,keymap也是很有亮点的,并且还支持多项目管理,tasks机制也值得看看。具体的内容我也仍然在持续摸索。

目前使用体验还不错,我还会继续折腾下去(