Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ macOS 和 Windows 可以用不同的 UI 技术实现,但用户看到的交互
反复询问子面板的最小、理想和最大尺寸。仅容器尚无确定宽高时读取内容
的理想尺寸。这个边界对应 IDEA `ThreeComponentsSplitter` 对现有组件
设置 bounds 的做法;达到尺寸限制后的相同结果不再写入拖动状态。
- 每个面板先由共享分隔容器包装成单个纵向布局容器,再交给三块矩形布局。
SwiftUI 的 `Group`、条件分支和 `ForEach` 会展开为零到多个直接子视图,
`.frame` 不会把这些子视图合成一个面板。#1564 原先达到 200 条时的“更多提示”暴露了该问题;
现已移除提示并自动分页,“文件列表 + 失败重试”仍须只占一个上方面板,下方预览另占一个面板。
不要让业务调用方逐处补包装,也不要遇到子视图数不是三个就跳过布局后
任由内容重叠。内部纵向容器只处理该面板的内容,外层仍直接分配确定边界;
空的加载状态同样保留面板边界。回归覆盖空、单项和多项面板,以及真实搜索
弹窗在 199/200 条结果时的列表、预览、鼠标选择和键盘滚动。
- 尺寸读取器的闭包仅处理可见宽高。全文测量、Diff 折叠与双栏规划、Git
文件树构建等数据计算放在闭包外,数据变化时更新;长 Diff 的代码文本使用固定宽度的原生文本容器,行号与连接带按
可见区域绘制;折叠和导航定位仍用 `LazyVStack`,不能用普通 `VStack` 布置整份文件。否则保持父视图不重建
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- **上传不互相拖住**:上传表只登记和移除身份,不把文件打开、写入、同步、发布或删除放在全局锁里。每个文件独立同步,同一上传的并发操作返回 `BUSY`;断线、取消和到期先标记取消,由仍在执行的工作线程保管并最终删除暂存文件。一个慢文件不能阻止另一个文件完成,释放动作也不能等慢文件的锁。
- **stdio 与启动**:语言、构建和 VCS 提供者复用 Core 的协议引擎,远程通道保留有界输入输出与原连接。异步部署、工具探测和启动在工作线程进行,启动期间保留有界文档快照与文件变更,停止后到达的会话先销毁再删除包。仅映射协议定义的 URI 和 VCS 路径字段,参数、文本与插件自定义值保持原样。依赖本机原生集成的 Java 仍明确拒绝,不能伪装成通用 stdio 已支持。
- **启动中取消不能依赖空资源表**:服务端的连接读取线程先登记进程、终端和监听的启动身份,再交给工作线程。后续关闭先记录在这个身份上,由创建者在发布资源后执行清理;即使关闭请求因工作线程已满返回 `BUSY`、客户端停止等待或断线,清理责任也不消失。关闭等待最多 60 秒,但到期不会撤销已经记录的清理;不同资源仍可并行,注册表锁不覆盖磁盘、启动或结束进程树。无效参数不能触发取消,后到的普通结束请求不能降低已请求的强制结束。这样修复的是所有通用后端的生命周期,不需要引入 Java/JDT 等语言专属语义。
- **常驻服务启动只允许一个代理创建进程**:daemon 的存活锁只能防止同时服务,不能阻止竞争失败的启动进程在旧会话关闭后接管锁。代理在启动前先取得同一身份的 `.start` 文件锁,再检查 socket,已有服务就复用;锁覆盖创建进程至 socket 可连接,等待使用原有启动期限。`.start` 位于远端用户私有状态目录的 `run/`,进程关闭文件即释放锁,文件本身保留,避免删除后不同代理锁住不同 inode;它属于远端会话协调状态,不可跨工作树复用,不写安装包,不影响代码签名或 Sparkle delta。验证启动锁互斥、期限、释放及并发连接收到关闭确认后服务确实退出,不用延长测试超时掩盖残留进程。
- **断线与会话结束不同**:常驻会话仅断线时保留终端和进程供重连;非驻留连接断开、显式关闭会话或空闲到期时,迟到发布的资源必须由创建者清理。结束标记在清理现有资源表之前设置,否则慢启动可能在清理后才出现,尤其是常驻会话不能再把它当成“等待重连”的合法资源。

### 正确做法
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,36 @@ Commit/Shelf 页签的留白以同一 Community revision 的 `IslandsUICustomiza

### Search Everywhere

#1564 的增量搜索复用 Core 已有请求事件和取消机制:先扫描、发布文件名,再构建正文/符号索引。
首次候选立即发送,后续名称/类/符号候选按约 100ms 合并,并在来源结束时刷新;每批是替换快照,
先按原有匹配度排序再限制每来源与总条数,较晚的优质候选不会被早期截断丢弃。
正文结果和 Git 忽略状态仍随最终响应到达;中间缺失的忽略状态保持未知。
macOS 使用只保留最新快照的 AsyncStream,逐批核对查询代次、工作区/来源有效性和任务取消,
旧任务结束不能清掉新任务的 loading;重置/关闭也撤销 Core 操作。
复用 RustCoreCancellation 的 requestStarted 握手处理注册前取消,阻塞的 C 调用放到 GCD worker。
列表及用户主动选择按结果 ID 识别,追加更高排名结果不会把预览切到同序号的另一文件。
未进行鼠标或键盘导航时,`nil` 表示默认跟随当前首项;不能把临时首项自动写成主动选择,
否则本地 Actions 先到或较差候选先到时,等待完成后 Enter 仍会激活旧项。主动选择失效才清回
默认状态。原生回归分别控制 Actions/文件与较差/更优候选的到达顺序,核对预览和 Enter。
保留 180ms 输入防抖和索引更新/失效屏障,冷名称索引仍须先完成轻量枚举;不宣称固定首屏延迟。
不引入新的搜索线程池、缓存或任意等待关键来源的排序延迟。参考
[ThrottlingListenerWrapper.java](https://github.com/JetBrains/intellij-community/blob/master/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java)
的默认 100ms 合并窗口,但不复制 IDEA 不同贡献者的等待策略。
Rust 模拟时间验证 99/100ms 边界、最终 flush 与较晚优质候选;实际桥接验证名称先于符号和取消。
模块回归覆盖流式期间 loading、替换查询、reset、取消与突发快照的最终结果;原生深浅主题
40/199/200 条结果同时核对分栏、首批可交互及追加结果后的选择/预览/滚动。
继续修复移除“更多”提示和每来源 200 条总上限:macOS 每来源每页 50 条,复用
`LitheScrollLoadTrigger` 与共享懒列表,靠近底部自动续查,无需点击加载。Core 使用绑定工作区、
查询、选项、来源及排除规则的游标;名称/符号仅保留最多 51 个候选,正文按路径/行推进并读取
一个额外命中判断耗尽,不预先搜全量再由 UI 切片,也不另建常驻搜索会话缓存。
首屏仍流式替换,续页完成后按 ID 去重追加;按页保持来源排名,动作位于首批名称之后,
防止后续类/符号或动作移动已显示行。重复游标、无新增项、耗尽停止;失败保留行与游标,
暂停自动加载,只提供已有 i18n Retry 操作。新查询/来源取消旧分页,防抖期间保留当前列表。
取舍:名称/符号每页仍扫描已有索引,50 条减少候选缓存/排序与首屏渲染,不承诺索引扫描变快;
游标读取实时索引,游标之前新插入的匹配需重新查询。原生深浅主题自动滚动加载 257 文件,
核对去重、非零像素偏移与预览不跳;Core 覆盖四来源超过 200、恰好整页、空页与跨查询游标拒绝。
Windows 仍消费旧有最终响应,未实现或验收其流式展示和自动分页。

双 Shift 搜索窗口沿用 Community `c7f91397daa3a961b4e78bc634fe467a0a7d9ade`
的 `SearchEverywhereUI.java`、`SENewUIHeaderView.kt`、`BigPopupUI.java`、
`IslandsUICustomization.kt`、`IslandsTabPainter.kt` 与 `ManyIslandsDark/Light.theme.json`。
Expand Down Expand Up @@ -147,8 +177,8 @@ Search Everywhere 可显式包含生成目录的**文件名**,但不把 `.buil
中的几十 GB 内容和二进制加入正文/符号索引。额外索引仅在内存中保存路径,沿用
Core 索引失效与文件增量事件;未被 watcher 监视的生成目录新增条目需刷新索引。
Git 元数据、嵌套工作树、自定义隐藏目录及路径 glob 仍被排除;该开关覆盖内置生成
目录名称排除,不改变项目树、正文搜索、替换和 watcher 的范围。每来源 200 项、
四类总计 800 项是有界候选,不等于 IDEA 的无限分页或完整项目模型。
目录名称排除,不改变项目树、正文搜索、替换和 watcher 的范围。Search Everywhere 按每来源
50 项分页;旧同步调用保留自身上限。自动分页不等于 IDEA 的完整项目模型。

漏斗按 `SearchEverywhereFiltersAction` 控制当前已实现的 Classes/Files/Symbols/Actions,
底部支持 All/None/Invert,复用 `litheDropdown` 的原生子面板。All 页包含选中来源的
Expand Down
35 changes: 31 additions & 4 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {

struct SearchResponsePayload: Decodable, Sendable {
let matches: [SearchMatchPayload]
var nextCursor: String? = nil

func makeResults(at rootURL: URL) -> [FileSearchResult] {
matches.map { match in
Expand All @@ -189,7 +190,8 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
fileMatches: results.filter { $0.kind == .file },
classMatches: results.filter { $0.kind == .type },
symbolMatches: results.filter { $0.kind == .symbol },
contentMatches: results.filter { $0.kind == .content }
contentMatches: results.filter { $0.kind == .content },
nextCursor: nextCursor
)
}
}
Expand Down Expand Up @@ -1560,6 +1562,9 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
let hiddenDirectoryNames: [String]
let hiddenFilePatterns: [String]
var includeGeneratedFileNames = false
var pageSize: Int? = nil
var cursor: String? = nil
var resultKinds: [String]? = nil
}

private struct ReplacementPreviewRequest: Encodable {
Expand Down Expand Up @@ -2558,9 +2563,11 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
fileMask: String = "",
hiddenDirectoryNames: [String] = [],
hiddenFilePatterns: [String] = [],
includeGeneratedFileNames: Bool = false
includeGeneratedFileNames: Bool = false,
operationID: String? = nil,
onEvent: (@Sendable (String) -> Void)? = nil
) -> SearchResponsePayload? {
execute(
let result: Result<SearchResponsePayload, CoreCallError> = executeResult(
command: "workspace.searchEverywhere",
payload: SearchRequest(
root: rootURL.standardizedFileURL.path,
Expand All @@ -2576,8 +2583,28 @@ struct RustCoreBridge: Sendable, IncrementalLanguageServerRuntimeCore {
hiddenDirectoryNames: hiddenDirectoryNames,
hiddenFilePatterns: hiddenFilePatterns,
includeGeneratedFileNames: includeGeneratedFileNames
)
),
operationID: operationID, onEvent: onEvent
)
return try? result.get()
}

func searchEverywherePage(
at rootURL: URL, query: String, options: ProjectSearchOptions,
hiddenDirectoryNames: [String], hiddenFilePatterns: [String], includeGeneratedFileNames: Bool,
kinds: Set<SearchResultKind>, cursor: String?, operationID: String,
onEvent: @escaping @Sendable (String) -> Void
) -> Result<SearchResponsePayload, CoreCallError> {
let size = SearchEverywhereResults.pageSize
return executeResult(command: "workspace.searchEverywhere", payload: SearchRequest(
root: rootURL.standardizedFileURL.path, query: query, caseSensitive: options.caseSensitive,
wholeWords: options.wholeWords, regularExpression: options.regularExpression,
maxResults: size * 4, maxFileResults: kinds.contains(.file) ? size : 0,
maxContentResults: kinds.contains(.content) ? size : 0,
maxSymbolResults: !kinds.isDisjoint(with: [.type, .symbol]) ? size : 0,
fileMask: options.fileMask, hiddenDirectoryNames: hiddenDirectoryNames,
hiddenFilePatterns: hiddenFilePatterns, includeGeneratedFileNames: includeGeneratedFileNames,
pageSize: size, cursor: cursor, resultKinds: kinds.map(\.rawValue).sorted()), operationID: operationID, onEvent: onEvent)
}

func previewReplacement(
Expand Down
52 changes: 50 additions & 2 deletions macos/Sources/Lithe/Core/Rust/RustWorkspaceOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ struct RustWorkspaceOperations: WorkspaceOperations, Sendable {
options: ProjectSearchOptions,
visibilityRules: FileVisibilityRules,
includeGeneratedFileNames: Bool = false,
kinds: Set<SearchResultKind> = [.file, .type, .symbol, .content]
kinds: Set<SearchResultKind> = [.file, .type, .symbol, .content],
operationID: String? = nil,
onEvent: (@Sendable (String) -> Void)? = nil
) -> SearchEverywhereResults? {
core.searchEverywhere(
at: rootURL,
Expand All @@ -121,7 +123,8 @@ struct RustWorkspaceOperations: WorkspaceOperations, Sendable {
fileMask: options.fileMask,
hiddenDirectoryNames: visibilityRules.hiddenDirectoryNames,
hiddenFilePatterns: visibilityRules.hiddenFilePatterns,
includeGeneratedFileNames: includeGeneratedFileNames
includeGeneratedFileNames: includeGeneratedFileNames,
operationID: operationID, onEvent: onEvent
)?.makeEverywhereResults(at: rootURL)
}

Expand Down Expand Up @@ -229,6 +232,51 @@ extension RustWorkspaceOperations: SearchOperations {
searchEverywhere(at: rootURL, query: query, options: options, visibilityRules: FileVisibilityRules(searchRules: visibilityRules), includeGeneratedFileNames: includeGeneratedFileNames, kinds: kinds)
}

func searchEverywhere(
at rootURL: URL, query: String, options: ProjectSearchOptions,
visibilityRules: SearchVisibilityRules, includeGeneratedFileNames: Bool,
kinds: Set<SearchResultKind>,
onResults: @escaping @Sendable (SearchEverywhereResults) -> Void
) async -> SearchEverywhereResults? {
try? await searchEverywherePage(at: rootURL, query: query, options: options, visibilityRules: visibilityRules,
includeGeneratedFileNames: includeGeneratedFileNames, kinds: kinds, cursor: nil, onResults: onResults)
}

func searchEverywherePage(
at rootURL: URL, query: String, options: ProjectSearchOptions,
visibilityRules: SearchVisibilityRules, includeGeneratedFileNames: Bool,
kinds: Set<SearchResultKind>, cursor: String?,
onResults: @escaping @Sendable (SearchEverywhereResults) -> Void
) async throws -> SearchEverywhereResults {
struct Batch: Decodable {
let type: String
let results: RustCoreBridge.SearchResponsePayload?
}
let operationID = UUID().uuidString
let cancellation = RustCoreCancellation(core: core, operationID: operationID)
return try await withTaskCancellationHandler {
try Task.checkCancellation()
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
guard !cancellation.isCancelled else { continuation.resume(throwing: CancellationError()); return }
let results = core.searchEverywherePage(
at: rootURL, query: query, options: options,
hiddenDirectoryNames: visibilityRules.hiddenDirectoryNames, hiddenFilePatterns: visibilityRules.hiddenFilePatterns,
includeGeneratedFileNames: includeGeneratedFileNames, kinds: kinds,
cursor: cursor, operationID: operationID, onEvent: { raw in
cancellation.receive(raw)
guard !cancellation.isCancelled,
let batch = try? JSONDecoder().decode(Batch.self, from: Data(raw.utf8)),
batch.type == "searchResults", let results = batch.results else { return }
onResults(results.makeEverywhereResults(at: rootURL))
})
if cancellation.isCancelled { continuation.resume(throwing: CancellationError()) }
else { continuation.resume(with: results.map { $0.makeEverywhereResults(at: rootURL) }) }
}
}
} onCancel: { cancellation.cancel() }
}

func previewReplacement(
at rootURL: URL,
query: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ struct LitheSplitPaneView<Sized: View, Flexible: View>: View {

@ViewBuilder
private var sizedPane: some View {
let pane = sized.frame(maxWidth: .infinity, maxHeight: .infinity,
// A builder can emit zero or multiple views; the split still needs one child per pane.
let pane = VStack(spacing: 0) { sized }.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: axis == .horizontal
? (placement == .leading ? .leading : .trailing)
: (placement == .leading ? .top : .bottom))
Expand All @@ -150,10 +151,11 @@ struct LitheSplitPaneView<Sized: View, Flexible: View>: View {

@ViewBuilder
private var flexiblePane: some View {
let pane = VStack(spacing: 0) { flexible }
if axis == .horizontal {
flexible.frame(minWidth: flexibleMinimum, maxWidth: .infinity)
pane.frame(minWidth: flexibleMinimum, maxWidth: .infinity)
} else {
flexible.frame(minHeight: flexibleMinimum, maxHeight: .infinity)
pane.frame(minHeight: flexibleMinimum, maxHeight: .infinity)
}
}

Expand Down
Loading
Loading