Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
# Linux runs the full suite and packaging; Windows runs the tests once,
# for its paths, file URIs and the read-only extracted JDK copies.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Check formatting
if: runner.os == 'Linux'
run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
- name: Vet
run: go vet ./...
- name: Test
run: go test -race -count=1 ./...

package:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Check manifest and build the archives
run: node scripts/package.mjs
- name: Validate against Lithe Store rules
run: |
git clone --depth 1 https://github.com/Lithe-IDEA/Lithe-store "$RUNNER_TEMP/store"
node --input-type=module -e "
import { readPackageManifests, checkManifest } from '$RUNNER_TEMP/store/scripts/validate-index.mjs';
import { readdirSync, readFileSync } from 'node:fs';
const manifest = JSON.parse(readFileSync('io.github.lithe-idea.java-navigation/plugin.json', 'utf8'));
const entry = { id: manifest.id };
let failed = false;
for (const name of readdirSync('dist')) {
const { manifests, unsafe } = readPackageManifests('dist/' + name, entry.id);
const problems = [...unsafe, ...checkManifest(manifests, entry, manifest)];
for (const problem of problems) { console.error(name + ': ' + problem); failed = true; }
}
process.exit(failed ? 1 : 0);
"
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Release

# Push a tag v<version> matching plugin.json to publish a GitHub Release.
# Lithe Store's tracker picks up stable releases and opens index pull requests.
# Tags listed in PRERELEASE_TAGS below are published as pre-releases, which the
# tracker skips; they need a manually submitted Store entry. Keep the plain
# v<version> tag. docs/releases/<tag>.md, when present, lists the changes.
on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Test
run: go test -count=1 ./...
- name: Build archives
run: |
set -o pipefail
node scripts/package.mjs --expect-tag "$GITHUB_REF_NAME" | tee "$RUNNER_TEMP/checksums.txt"
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
# v0.0.1 follows plugin contract v0, which only the new desktop
# client installs, and that client ships as desktop-v* pre-releases.
PRERELEASE_TAGS: v0.0.1
run: |
prerelease=false
for tag in $PRERELEASE_TAGS; do
if [[ "$tag" == "$GITHUB_REF_NAME" ]]; then prerelease=true; fi
done
{
echo "兼容性:按插件契约 v0(schemaVersion 0、pluginAPIVersion 0)编写,需要新版 Lithe 桌面端(desktop-v* 预发布)。旧版 macOS Lithe 无法安装。"
echo
if [[ -s "docs/releases/$GITHUB_REF_NAME.md" ]]; then
cat "docs/releases/$GITHUB_REF_NAME.md"
echo
fi
if [[ "$prerelease" == true ]]; then
echo "安装:预发布版本,Lithe Store 自动追踪会跳过,暂不收录 Store。下载与你的系统和芯片对应的 ZIP,在 设置 → 插件 中点击页面标题栏右侧的 从文件安装…,选中 ZIP 后核对 SHA-256 并确认。"
else
echo "安装:Lithe Store(https://github.com/Lithe-IDEA/Lithe-store)中的 Java Navigation 条目合并后,可在 设置 → 插件 → 插件市场 中搜索 Java Navigation 并安装。"
fi
echo
echo "SHA-256:"
echo '```'
sed 's#^dist/##' "$RUNNER_TEMP/checksums.txt"
echo '```'
} > "$RUNNER_TEMP/notes.md"
release_flags=()
if [[ "$prerelease" == true ]]; then
release_flags+=(--prerelease)
fi
gh release create "$GITHUB_REF_NAME" dist/*.zip --title "Java Navigation $GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md" "${release_flags[@]}"
95 changes: 94 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,96 @@
# Java Navigation for Lithe

Lithe 的轻量 Java 跳转插件(不依赖 JDT LS)。首个版本见 Draft PR。
为 [Lithe](https://github.com/1lck/Lithe-IDEA) 提供**不依赖 JDT LS 的轻量 Java 跳转**:跳转到定义、查找引用和文档结构。
插件自带一个用 Go 写的小型语言服务器,不需要 JDK 运行时、不导入 Maven/Gradle 构建,打开项目后第一次跳转约 0.2 秒就能用,
常驻内存几十 MB(JDT LS 通常要 3–5 GB,首次导入几十秒到几分钟)。
这是一个**社区插件**,按插件契约 v0 编写。

> English: lightweight Java navigation for Lithe without JDT LS — go to definition, find references and the
> document outline, served by a small Go language server that keeps an index of declarations only. No JDK
> runtime, no build import, tens of MiB of memory.

> **预发布**:按插件契约 v0(`schemaVersion: 0`、`pluginAPIVersion: 0`)编写,只能装进新版 Lithe 桌面端
> (`desktop-v*` 预发布),旧版 macOS Lithe 无法安装。

## 功能

| 功能 | 说明 |
| --- | --- |
| 跳转到定义 | 类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和调用参数个数解析,支持调用链 `a.b().c` |
| Lombok | `@Data`/`@Getter`/`@Setter` 生成的访问器跳到字段,`@Builder`、`@Slf4j`、构造器注解跳到注解本身(与 JDT LS 一致) |
| 跳进 JDK | 项目里找不到的名字到本机 JDK 的 `src.zip` 里找,`String`、`List`、`hashCode()` 等都能跳,并能在 JDK 源码里继续跳 |
| 查找引用 | 只列出能跳回这个声明的出现,所以列表和在任一行上跳转的结果一致;`package`/`import` 不算引用 |
| 文档结构 | 类型、成员和嵌套类型的大纲(`textDocument/documentSymbol`) |
| 未保存的修改 | 打开的文件按编辑器里的内容索引,未保存的新方法也能从其他文件跳过去 |

## 怎么工作

- 索引只保留**声明**(类型、父类型、成员及其书写的类型、参数个数、Lombok 生成的成员),不保留语法树,也不做类型绑定。
- 第一次跳转时才建索引:在 Git 仓库里用 `git ls-files` 列出已跟踪和未跟踪的文件(跳过 `.gitignore` 忽略的),
否则遍历目录并跳过隐藏目录与 `target`、`build`、`out`、`bin`、`node_modules`。超过 2 MiB 的文件跳过。
- 之后每次查询如果距上次扫描超过 2 秒,就按文件大小和修改时间增量重扫,其他工具改的文件也能反映出来。
- JDK:按 `JAVA_HOME` → 系统默认 JDK(macOS 的 `/usr/libexec/java_home`,其他系统 `PATH` 上的 `java`)→ 常见安装目录
(含 IntelliJ 的 `~/.jdks`、SDKMAN!)的顺序找第一个带 `src.zip` 的 JDK。只预先列出 zip 目录,某个类第一次被查到时才解析。
跳进 JDK 时把那一个源码文件解压成只读副本,放在 Lithe 为插件分配的状态目录(`${stateDir}`)里,卸载插件时一起删除;
不写插件包或应用安装目录。

解析规则来自 [Viff](https://github.com/1lck/viff)(MIT),见 `internal/codenav/UPSTREAM.md` 和 `THIRD_PARTY_NOTICES.md`。

## 实测(Apple M4,macOS,JDK 17)

在 `src/main/java` 里随机取 1500 个标识符(不含关键字、字符串里的词、`package`/`import` 行),两个种子:

| 项目 | Java 文件 | 能跳转的比例 | 其中跳进 JDK | 单次 p50 / p99 | 第一次跳转(含建索引) | 服务器常驻内存 |
| --- | --- | --- | --- | --- | --- | --- |
| Nacos | 2886 | 93.9% / 94.9% | 333 / 326 | 0.07 ms / 0.8–1.0 ms | 0.12 s | 约 74–76 MiB |
| ruoyi-vue-pro | 6355 | 54.3% / 53.3% | 198 / 207 | 0.06 ms / 0.5–0.6 ms | 0.17 s | 约 75–85 MiB |

查找引用 20 次,p50 约 56 ms,最慢 0.13 s。

宿主联调:用 Lithe 桌面端固定的 Rust Core(`47c9859`)校验 `plugin.json`、规划 `packaged` 启动、经 Core 启动语言服务器,
会话进入 `ready` 并声明 `definition`、`references`、`documentSymbols`,三种请求都返回正确位置;跳进 JDK 时只在 `${stateDir}`
写入解压副本,插件包文件的 SHA-256 前后一致。ruoyi-vue-pro 跳不了的几乎全是依赖库的注解和它们的属性:
Swagger 的 `@Schema(description, example, requiredMode)`、Lombok 的 `@Data`、校验注解 `@NotNull`、EasyExcel、Spring 注解等,
见下面的“限制”。

## 限制

- **不读依赖库**:Spring、MyBatis、Lombok、Swagger 等依赖里的声明找不到(返回“没有定义”,不会跳错)。
计划:从 Maven/Gradle 插件取 classpath,按需读取 `sources.jar`,没有源码时显示只有签名的存根。
- **没有类型推断**:重载只按参数个数选择;lambda 参数的类型和泛型替换后的返回类型推不出来,
例如 `list.get(0).getName()` 里的 `getName`、`users.stream().map(u -> u.getName())` 里的 `getName`;
以字面量为限定符的调用(`"".trim()`)也推不出类型。
- **只认一个 JDK**:用的是上面顺序找到的 JDK,不一定是项目编译用的版本。
- **引用不含重写和实现**:查找方法的引用不包括子类重写的方法和通过接口调用到的其他声明。
- **生成代码**:注解处理器生成到 `target/generated-sources` 的类通常被 `.gitignore` 忽略,不进索引。
- **和 Java Support(JDT LS)同时安装**:两个插件都为 Java 声明语言服务器,宿主怎么分配请求还没有定义,暂时不要同时启用。

## 开发

需要 Go 1.27.3 和 Node.js 22(只用于打包)。

```sh
go test ./... # 单元测试和端到端协议测试
go test -race ./...
node scripts/package.mjs # 交叉编译四个平台并打包到 dist/
```

`dist/` 里每个平台一个 zip(`darwin-arm64`、`darwin-x64`、`windows-x64`、`windows-arm64`),包内只有
`plugin.json`、`bin/java-navigation`(Windows 是 `bin/java-navigation.exe`,对应平台包的 `plugin.json` 也写这个路径)、
`LICENSE` 和 `THIRD_PARTY_NOTICES.md`。

语言服务器可以单独运行调试:

```sh
go run ./cmd/java-navigation --state-dir /tmp/jn-state # 通过 stdin/stdout 说 LSP,日志写 stderr
go run ./cmd/java-navigation --version
```

## 发布

推送与 `plugin.json` 版本一致的 tag `v<version>`,`release.yml` 会打包并创建 GitHub Release。
`PRERELEASE_TAGS` 里列出的 tag 发布为预发布,Lithe Store 的自动追踪会跳过,需要手工提交收录。

## 许可

Apache License 2.0,见 `LICENSE`。`internal/codenav` 等来自 Viff 的代码按 MIT 许可,见 `THIRD_PARTY_NOTICES.md`。
37 changes: 37 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Third-party notices

Java Navigation is licensed under the Apache License 2.0 (see `LICENSE`). It includes
code from the following project, under that project's license.

## Viff

- Source: https://github.com/1lck/viff
- Used in: `internal/codenav/` (Java declaration index, resolution and usages; copied
unchanged except for files marked as Lithe additions), `internal/jdk/find.go` and
`internal/jdk/sources.go` (JDK discovery and `src.zip` reading, adapted). See
`internal/codenav/UPSTREAM.md`.
- License: MIT

```
MIT License

Copyright (c) 2026 Viff contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
49 changes: 49 additions & 0 deletions cmd/java-navigation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Command java-navigation is a lightweight Java language server for Lithe. It
// answers go to definition, find references and the document outline from a
// resident index of declarations, without a JDK runtime, a build import or
// JDT LS. It speaks LSP over standard input and output.
package main

import (
"errors"
"flag"
"fmt"
"log"
"os"

"github.com/Lithe-IDEA/lithe-plugin-java-navigation/internal/jdk"
"github.com/Lithe-IDEA/lithe-plugin-java-navigation/internal/lsp"
)

// version is set at build time with -ldflags "-X main.version=...".
var version = "dev"

func main() {
state := flag.String("state-dir", "", "writable directory for extracted JDK sources (the host's ${stateDir})")
printVersion := flag.Bool("version", false, "print the version and exit")
noJDK := flag.Bool("no-jdk", false, "do not navigate into the JDK's sources")
flag.Bool("stdio", true, "speak LSP over standard input and output (the only transport)")
flag.Parse()
if *printVersion {
fmt.Println("java-navigation", version)
return
}
// Standard output carries the protocol; diagnostics go to standard error,
// which the host records in the server's log.
logger := log.New(os.Stderr, "java-navigation: ", log.LstdFlags|log.Lmsgprefix)
opt := lsp.Options{Name: "java-navigation", Version: version, State: *state, Logf: logger.Printf}
if !*noJDK && *state != "" {
opt.FindJDK = jdk.FindJDK
}
if *state == "" {
logger.Printf("no --state-dir: navigation into the JDK is off")
}
err := lsp.New(os.Stdin, os.Stdout, opt).Run()
switch {
case errors.Is(err, lsp.ErrExitWithoutShutdown):
os.Exit(1)
case err != nil:
logger.Printf("%v", err)
os.Exit(1)
}
}
19 changes: 19 additions & 0 deletions docs/releases/v0.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
0.0.1 是首个版本:不依赖 JDT LS 的轻量 Java 跳转,按插件契约 v0 编写。

功能:
- 跳转到定义:类型、方法、构造器、字段、枚举常量、局部变量、参数、类型参数、注解及其属性;按导入、同包、通配导入、嵌套、继承链和参数个数解析,支持调用链。
- Lombok:访问器跳到字段,`@Builder`、`@Slf4j` 和构造器注解生成的成员跳到注解。
- 跳进本机 JDK 的 `src.zip`,并能在 JDK 源码里继续跳;JDK 源码以只读副本打开,放在插件状态目录里。
- 查找引用:只列出能跳回该声明的出现。
- 文档结构(大纲)。
- 打开的文件按编辑器里未保存的内容索引。

平台:macOS(Apple Silicon、Intel)、Windows(x64、ARM64),每个平台一个 ZIP。

权限:`languageServer.launch`。

限制:不读依赖库(Spring、MyBatis、Lombok、Swagger 等的声明找不到);没有 lambda 参数和泛型替换的类型推断;不要与 Java Support(JDT LS)同时启用。

兼容性:
- 只能装进新版 Lithe 桌面端(`desktop-v*` 预发布),旧版 macOS Lithe 无法安装。
- 本版本是预发布,Lithe Store 的自动追踪会跳过它。
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Lithe-IDEA/lithe-plugin-java-navigation

go 1.27.3
20 changes: 20 additions & 0 deletions internal/codenav/UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# codenav 的上游

本目录的 Java 声明索引、解析和使用处查找来自 [Viff](https://github.com/1lck/viff) 的
`internal/codenav`(MIT 许可,见仓库根目录 `THIRD_PARTY_NOTICES.md`)。

- 来源:Viff 工作副本 `viff-main`(2026-11-06),当时这部分代码还没有推到 `1lck/viff` 的任何分支,
所以这里没有对应的上游提交。上游发布后,把这一行换成对应的提交 SHA。
- 设计取舍与实测数据:Viff 的 ADR 0040(跳转到声明与使用处)和 ADR 0041(跳转到 JDK)。

## 同步规则

- 除下面列出的文件,本目录的 `.go` 文件与上游**逐字节一致**,不要在这里直接改;需要改解析规则时,
先在 Viff 修(先补一条能失败的测试),再整目录拷回来。
- Lithe 新增的代码放在单独的文件里,文件开头注明“This file is a Lithe addition”:
- `symbols.go`:文档结构(`textDocument/documentSymbol`)。
- 同步后运行 `go test ./...`。

`internal/jdk/find.go` 改编自 Viff 的 `internal/platform/jdk.go`(去掉了 Viff 的进程辅助函数,
只在 macOS 上起的 `java_home` 进程本来就没有控制台窗口);`internal/jdk/sources.go` 改编自 Viff 的
`internal/app/jdk.go`,加了把单个源码文件解压成只读副本的 `Extract`。
Loading
Loading