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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ rust/target/
DerivedData/
/windows/build*/
dist/
.artifacts/
Fixtures/**/target/
*.xcuserstate
*.xcuserdata/
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ When an external AI tool changes a project, Lithe helps you locate the affected

## Use Lithe

Lithe requires macOS 13 or later. Java project features require a JDK; JDK 17 or JDK 21 is recommended. Maven projects need either a project `mvnw` or a system Maven installation. Lightweight completion does not start an external process; when a matching language server is installed, Lithe routes the capabilities it actually advertises through the shared Rust LSP Core. See the [language tooling and LSP architecture](./docs/architecture/language-tooling.md) for provider configuration and compatibility details.
Lithe requires macOS 13 or later. Java project features require JDK 17 or newer; JDK 17 or JDK 21 is recommended. Release packages include Eclipse JDT Language Server for Java completion, navigation, references, and diagnostics, so JDTLS does not need to be installed separately. Maven projects need either a project `mvnw` or a system Maven installation. Lightweight completion does not start an external process; Lithe routes the capabilities a running language server actually advertises through the shared Rust LSP Core. See the [language tooling and LSP architecture](./docs/architecture/language-tooling.md) for provider configuration and compatibility details.

Download the latest macOS `.dmg` from [GitHub Releases](https://github.com/1lck/Lithe-IDEA/releases/latest). If a release provides architecture-specific installers, choose `arm64` for Apple silicon or `x86_64` for an Intel Mac. Open the disk image, drag `Lithe.app` into `/Applications`, and launch it.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Lithe 是一款面向 AI 时代打造的轻量通用型 IDE。它面向多语言

## 如何使用

Lithe 需要 macOS 13 或更高版本。Java 项目功能需要 JDK,推荐使用 JDK 17 或 JDK 21;Maven 项目需要项目自带 `mvnw` 或系统中可用的 Maven。轻量补全无需启动外部进程;安装相应语言服务器后,Lithe 会通过共享 Rust LSP Core 按服务器实际声明的能力提供语义功能。详细设计与自定义 provider 配置见[语言工具与 LSP 架构](./docs/architecture/language-tooling.md)。
Lithe 需要 macOS 13 或更高版本。Java 项目功能需要 JDK 17 或更高版本,推荐使用 JDK 17 或 JDK 21;正式安装包已包含 Eclipse JDT Language Server,可直接提供 Java 补全、跳转、引用和诊断,无需单独安装 JDTLS。Maven 项目需要项目自带 `mvnw` 或系统中可用的 Maven。轻量补全无需启动外部进程;Lithe 会通过共享 Rust LSP Core 按运行中服务器实际声明的能力提供语义功能。详细设计与自定义 provider 配置见[语言工具与 LSP 架构](./docs/architecture/language-tooling.md)。

从 [GitHub Releases](https://github.com/1lck/Lithe-IDEA/releases/latest) 下载最新的 macOS `.dmg`。如果该版本提供独立架构安装包,M 系列芯片选择 `arm64`,Intel 芯片选择 `x86_64`。打开磁盘映像,将 `Lithe.app` 拖入 `/Applications` 后启动。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ final class RuntimeSettingsFeatureModel: ObservableObject {
@Published private(set) var javaEnvironmentReport: JavaEnvironmentReport?
@Published private(set) var isDiscovering: Bool

var javaLanguageServerRuntimes: [JavaRuntimeCandidate] {
service.javaLanguageServerRuntimes
}

init(service: ProjectRuntimeService) {
self.service = service
_javaRuntimes = Published(initialValue: service.javaRuntimes)
Expand Down
108 changes: 108 additions & 0 deletions Sources/Lithe/Models/AppModel/AppModel+LanguageServerRuntime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Foundation

@MainActor
extension AppModel {
func chooseLanguageServerExecutable(providerName: String) -> URL? {
platformUI.chooseFile(
title: settings.language == .simplifiedChinese
? "选择 \(providerName) 语言服务器"
: "Choose \(providerName) language server",
prompt: settings.language == .simplifiedChinese ? "选择" : "Choose"
)
}

func openLanguageServerDownload(_ url: URL) {
platformUI.open(url)
}

func languageServerToolConfigurationDidChange(providerID: String) {
languageToolingFeature.toolConfigurationDidChange(providerID: providerID)
}

func isLanguageServerDisabledInCurrentWorkspace(providerID: String) -> Bool {
languageToolingFeature.isDisabled(providerID)
}

func setLanguageServerEnabled(_ enabled: Bool, providerID: String) {
if enabled {
languageToolingFeature.setEnabled(true, providerID: providerID)
} else {
languageToolingFeature.setEnabled(false, providerID: providerID)
}
}

var javaLanguageServerJDKPath: String {
settings.javaLanguageServerJDKPath
}

var detectedJavaLanguageServerJDKs: [JavaRuntimeCandidate] {
runtimeFeature.javaLanguageServerRuntimes
}

func selectJavaLanguageServerJDK(_ runtime: JavaRuntimeCandidate) {
applyJavaLanguageServerJDKPath(runtime.homePath)
}

func refreshJavaLanguageServerJDKs() async {
await runtimeFeature.refreshAvailableRuntimes()
}

func useAutomaticJavaLanguageServerJDK() {
applyJavaLanguageServerJDKPath("")
}

func chooseJavaLanguageServerJDK() {
guard let url = platformUI.chooseDirectory(
title: settings.language == .simplifiedChinese ? "选择 LSP 运行 JDK" : "Choose LSP Runtime JDK",
prompt: settings.language == .simplifiedChinese ? "选择" : "Choose"
) else { return }
Task { [weak self] in
guard let self else { return }
guard let runtime = await self.services.projectRuntimeService
.inspectJavaLanguageServerRuntime(atPath: url.path) else {
self.showNotification(self.settings.language == .simplifiedChinese
? "所选目录不是有效的 JDK Home"
: "The selected directory is not a valid JDK Home")
return
}
guard runtime.supportsJDTLS else {
self.showNotification(self.settings.language == .simplifiedChinese
? "JDTLS 需要 JDK 17 或更高版本;所选版本为 \(runtime.version)"
: "JDTLS requires JDK 17 or newer; the selected version is \(runtime.version)")
return
}
self.applyJavaLanguageServerJDKPath(url.standardizedFileURL.path)
}
}

func disableLanguageServerForCurrentWorkspace(providerID: String) {
languageToolingFeature.setEnabled(false, providerID: providerID)
}

func prepareJavaLanguageServerRuntimeIfNeeded(for document: EditorDocument) -> Bool {
let path = settings.javaLanguageServerJDKPath.trimmingCharacters(in: .whitespacesAndNewlines)
if services.projectRuntimeService.isJavaLanguageServerRuntimePrepared(overridePath: path) {
return true
}
guard javaLanguageServerRuntimePreparationPath != path else { return false }

javaLanguageServerRuntimePreparationTask?.cancel()
javaLanguageServerRuntimePreparationPath = path
javaLanguageServerRuntimePreparationTask = Task { [weak self, weak document] in
guard let self, let document else { return }
await self.services.projectRuntimeService.prepareJavaLanguageServerRuntime(
overridePath: path
)
guard !Task.isCancelled,
self.javaLanguageServerRuntimePreparationPath == path else { return }
self.javaLanguageServerRuntimePreparationTask = nil
self.javaLanguageServerRuntimePreparationPath = nil
_ = self.activateLanguageServerIfAvailable(for: document)
}
return false
}

private func applyJavaLanguageServerJDKPath(_ path: String) {
languageToolingFeature.selectJavaJDK(path)
}
}
74 changes: 6 additions & 68 deletions Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,79 +280,14 @@ final class AppModel: ObservableObject, Identifiable {
isSettingsPresented = true
}

func chooseLanguageServerExecutable(providerName: String) -> URL? {
platformUI.chooseFile(
title: settings.language == .simplifiedChinese
? "选择 \(providerName) 语言服务器"
: "Choose \(providerName) language server",
prompt: settings.language == .simplifiedChinese ? "选择" : "Choose"
)
}

func openLanguageServerDownload(_ url: URL) {
platformUI.open(url)
}

func languageServerToolConfigurationDidChange(providerID: String) {
languageToolingFeature.toolConfigurationDidChange(providerID: providerID)
}

func isLanguageServerDisabledInCurrentWorkspace(providerID: String) -> Bool {
languageToolingFeature.isDisabled(providerID)
}

func setLanguageServerEnabled(_ enabled: Bool, providerID: String) {
if enabled {
languageToolingFeature.setEnabled(true, providerID: providerID)
} else {
languageToolingFeature.setEnabled(false, providerID: providerID)
}
}

var javaLanguageServerJDKPath: String {
settings.javaLanguageServerJDKPath
}

var detectedJavaLanguageServerJDKs: [JavaRuntimeCandidate] {
runtimeFeature.javaRuntimes
}

func selectJavaLanguageServerJDK(_ runtime: JavaRuntimeCandidate) {
applyJavaLanguageServerJDKPath(runtime.homePath)
}

func refreshJavaLanguageServerJDKs() async {
await runtimeFeature.refreshAvailableRuntimes()
}

func chooseJavaLanguageServerJDK() {
guard let url = platformUI.chooseDirectory(
title: settings.language == .simplifiedChinese ? "选择 LSP 运行 JDK" : "Choose LSP Runtime JDK",
prompt: settings.language == .simplifiedChinese ? "选择" : "Choose"
) else { return }
guard services.projectRuntimeService.configuredJavaExecutableURL(overridePath: url.path) != nil else {
showNotification(settings.language == .simplifiedChinese
? "所选目录不是有效的 JDK Home"
: "The selected directory is not a valid JDK Home")
return
}
applyJavaLanguageServerJDKPath(url.standardizedFileURL.path)
}

private func applyJavaLanguageServerJDKPath(_ path: String) {
languageToolingFeature.selectJavaJDK(path)
}

func disableLanguageServerForCurrentWorkspace(providerID: String) {
languageToolingFeature.setEnabled(false, providerID: providerID)
}

private var documentFeatureObservation: AnyCancellable?
private var javaFeatureObservation: AnyCancellable?
private var springFeatureObservation: AnyCancellable?
private var navigationHistoryFeatureObservation: AnyCancellable?
private var isObjectWillChangeRelayScheduled = false
private var languageToolingObservation: AnyCancellable?
var javaLanguageServerRuntimePreparationTask: Task<Void, Never>?
var javaLanguageServerRuntimePreparationPath: String?
private var recentProjectsStore: RecentProjectsStore { services.recentProjectsStore }
private var workbenchLayoutStore: WorkbenchLayoutStore { services.workbenchLayoutStore }

Expand Down Expand Up @@ -1341,9 +1276,12 @@ final class AppModel: ObservableObject, Identifiable {
}

@discardableResult
private func activateLanguageServerIfAvailable(for document: EditorDocument) -> Bool {
func activateLanguageServerIfAvailable(for document: EditorDocument) -> Bool {
guard let workspaceURL,
let descriptor = languageProviderCatalog.provider(for: document.url) else { return false }
if descriptor.id == "java", !prepareJavaLanguageServerRuntimeIfNeeded(for: document) {
return false
}
if let ownership = services.pluginCatalog.languageSupport(for: document.url),
ownership.declaration.languageServerModuleID != nil {
let support = ownership.declaration
Expand Down
20 changes: 20 additions & 0 deletions Sources/Lithe/Models/Runtime/ProjectRuntimeModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct ProjectRuntimeSettings: Codable, Hashable, Sendable {
}

struct JavaRuntimeCandidate: Identifiable, Hashable, Sendable {
static let minimumJDTLSMajorVersion = 17

let homePath: String
let version: String
let vendor: String
Expand All @@ -36,6 +38,24 @@ struct JavaRuntimeCandidate: Identifiable, Hashable, Sendable {
let vendor = vendor.isEmpty ? "JDK" : vendor
return "\(vendor) \(version)"
}

var majorVersion: Int? {
let components = version
.split(whereSeparator: { !$0.isNumber })
.compactMap { Int($0) }
switch components.first {
case 1:
return components.count > 1 ? components[1] : nil
case let major?:
return major
case nil:
return nil
}
}

var supportsJDTLS: Bool {
majorVersion.map { $0 >= Self.minimumJDTLSMajorVersion } ?? false
}
}

struct MavenRuntimeCandidate: Identifiable, Hashable, Sendable {
Expand Down
12 changes: 8 additions & 4 deletions Sources/Lithe/Platform/MacOS/MacServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,15 @@ final class MacServiceContainer {
languageServerCore: rustCore,
languageServerExecutableResolver: { tools.executableURL(for: $0) },
languageServerRuntimeResolver: { descriptor in
descriptor.id == "java"
? runtimeService.configuredJavaExecutableURL(
overridePath: settings.javaLanguageServerJDKPath
guard descriptor.id == "java" else { return .notRequired }
guard let executableURL = runtimeService.javaLanguageServerExecutableURL(
overridePath: settings.javaLanguageServerJDKPath
) else {
return .unavailable(
"JDTLS requires JDK 17 or newer. Configure a compatible JDK in Language Server settings."
)
: nil
}
return .available(executableURL)
},
languageServerCacheDirectory: fileStorage.cacheDirectory()
.appendingPathComponent("Lithe/language-servers", isDirectory: true),
Expand Down
11 changes: 11 additions & 0 deletions Sources/Lithe/Platform/MacOS/Runtime/MacRuntimeToolDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import Foundation
/// package manager or installs anything on the user's behalf.
struct MacRuntimeToolDiscovery: RuntimeToolDiscovery {
private let homeDirectoryURL: URL
private let resourceDirectoryURL: URL?
private let isExecutable: @Sendable (URL) -> Bool

init(
homeDirectoryURL: URL = FileManager.default.homeDirectoryForCurrentUser,
resourceDirectoryURL: URL? = Bundle.main.resourceURL,
isExecutable: @escaping @Sendable (URL) -> Bool = {
FileManager.default.isExecutableFile(atPath: $0.path)
}
) {
self.homeDirectoryURL = homeDirectoryURL.standardizedFileURL
self.resourceDirectoryURL = resourceDirectoryURL?.standardizedFileURL
self.isExecutable = isExecutable
}

Expand All @@ -38,6 +41,14 @@ struct MacRuntimeToolDiscovery: RuntimeToolDiscovery {
))
}

if command == "jdtls", let resourceDirectoryURL {
add(
resourceDirectoryURL.appendingPathComponent("LanguageServers/jdtls/bin/jdtls"),
source: .bundled,
detail: "Bundled with Lithe"
)
}

// Project-local toolchains are preferred because they are reproducible
// and do not alter the user's global environment.
if let projectURL {
Expand Down
Loading
Loading