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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
## 决策

- 由 Rust Core 负责列目录。`workspace.snapshot` 接受 `includeTree: false`,这时只返回 `files`
和一个没有子项的 `root`。Windows 不传这个字段,行为不变。
和一个没有子项的 `root`。Windows 起初不传这个字段;后来 Windows 的文件清单调用方(`fffListFiles`、Lombok 导航)也改为传 `includeTree: false`,因为它们只用 `files`,Windows 文件树自己按目录读取,从不使用快照里的树(见 `2026-10-29-skip-python-environments-in-workspace-scans.md`)。
- `workspace.listDirectories` 一次可以列出多个相对路径的下一层。它与快照使用同一套过滤规则:
隐藏目录、隐藏文件、符号链接、嵌套工作树。
不存在、被隐藏或越过根目录的路径直接不返回,调用方不需要区分具体是哪种原因。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Agent 笔记:工作区扫描默认跳过 Python 环境和字节码

状态:已实现

## 先说结论

打开一个 Python 项目时,Core 会把整个工作区扫一遍(快照,`workspace.snapshot`),而虚拟环境(`.venv`、`venv`)和字节码缓存(`__pycache__`、`*.pyc`)占了绝大多数文件。
现在这四项和 `node_modules` 一样,写进 Rust Core 的内置隐藏名单:扫描时整个目录不再被读取,也不会进入搜索索引。
同时 Windows 请求快照时只要文件清单,不再让 Core 顺带构造整棵目录树。
开发者要记住:名单按名字匹配,名字不同的虚拟环境不会被识别;Windows 文件树仍然读磁盘目录,所以这次没有让 `.venv` 从 Windows 文件树里消失。

## 问题

- 一个典型的 Python 后端项目按旧规则会被扫出约 3.2 万个文件,其中约 2.7 万个在虚拟环境和 `__pycache__` 里。
这些文件不是用户写的代码,快速打开、全局搜索、Spring/MyBatis/Maven 索引都不需要它们。
- 打开项目时,Windows 会连续请求好几次快照(同步工作区、MyBatis、Spring、Maven 各一次,本地路径没有缓存),每次都重复扫描这几万个文件。
- Windows 请求快照时不传 `includeTree`,Core 默认还会构造并序列化整棵目录树。Windows 文件树按目录单独读取,从不使用这棵树,macOS 早在 #1245 就已经关掉它。

## 决策

### 正确做法

- `rust/lithe-core/src/project/files.rs` 的 `BUILT_IN_HIDDEN_DIRECTORIES` 增加 `.venv`、`venv`、`__pycache__`,`BUILT_IN_HIDDEN_FILE_PATTERNS` 增加 `*.pyc`。
这份名单同时决定快照、目录列举、搜索索引、文件监听事件是否相关,以及 Git 仓库发现要不要进入某个目录,所以改一处就覆盖这些入口。
- macOS 的 `FileVisibilityRules.swift` 保留自己的一份内置名单(设置页会把它显示出来),同步增加同样的四项,避免页面显示和实际规则不一致。
- 只需要文件清单的调用方请求 `workspace.snapshot` 时带 `includeTree: false`。Windows 的 `fffListFiles`(Quick Open、全局搜索、Spring/MyBatis/Maven 索引经由它取文件)和 Lombok 导航都已这样做。
- 新增类似的“依赖或生成物”目录时,加到 Core 的内置名单,而不是每个调用方各自过滤。

### 不要这样做

- 不要只在前端过滤结果。目录已经被读完、序列化、传给 WebView 之后再丢弃,扫描和内存的代价都已经付出了。
- 不要按 `.gitignore` 默认排除。见下面的备选方案。

## 考虑过的备选方案

### 读取 `.gitignore` 来决定隐藏什么

好处是项目自己声明的忽略项都能覆盖,不用逐个加名字。
没有采用的原因:非 Git 项目和没有写 `.gitignore` 的项目不受益;嵌套的 `.gitignore`、否定规则(`!`)和全局忽略文件的求值成本和正确性都要单独处理;被 Git 忽略但仍想搜索的文件(例如本地配置)会搜不到。
IntelliJ 的做法也是不自动排除:它只在用户编辑忽略文件时提示“是否把被忽略的目录标记为排除”(`IgnoredToExcludedSynchronizer`),由用户确认。

### 用 `pyvenv.cfg` 识别任意名字的虚拟环境

好处是 `env`、`.tox` 之类非常规名字也能被识别,不会误伤同名的普通目录。
没有采用的原因:名单是纯名字匹配,识别需要读目录内容,要改 `VisibilityRules::is_hidden` 的结构。这次先覆盖最常见的两个名字,以后需要时再扩展。

### 像 IntelliJ 那样新增“库根”类别

IntelliJ 把 `.venv` 登记成外部库:保留补全和跳转,但不算项目内容(`WorkspaceFileKind.EXTERNAL`)。
没有采用的原因:库根在 IntelliJ 里是被索引的,性能上不省。Lithe 的语言事实由语言服务器负责,Core 不应该再为依赖库维护第二份索引。如果以后要在文件树里区分展示,应当只做展示,不让它重新进入快照。

## 后果

- 收益:快照、搜索索引、文件监听不再处理虚拟环境和字节码;Python 项目打开时每次快照的文件数约降到原来的七分之一。
- 代价:名字不同的虚拟环境仍会被扫描,用户可以在隐藏路径设置里补一条规则。
- macOS 文件树按目录列举时用同一套规则,所以 `.venv` 在 macOS 文件树里也不再显示,和现在的 `node_modules` 一样。
- Windows 文件树使用前端的 `hiddenDirectoryPatterns`(默认值里有 `__pycache__` 和 `*.pyc`,没有 `.venv`),所以仍能看到 `.venv`。是否也隐藏它是另一个决定,需要同时处理设置迁移。
- 打开“包含生成文件名”的 Search Everywhere 选项时,内置隐藏目录会被重新遍历,`.venv` 也在其中,这与 `node_modules` 现有行为一致。
- Rust 里已有同类名单:`vcs/roots.rs`(Git 仓库发现)和 `execution/workspace_scan.rs`(运行扫描)本来就包含 `.venv`、`venv`、`__pycache__`,这次只是让工作区快照与它们一致。
- 需要重新评估的触发条件:出现不遵守常规命名的虚拟环境集中占用扫描,或者要在文件树里区分展示依赖库时。

## 验证

- `cargo test --lib workspace_snapshot_skips_python_environments_and_bytecode_caches`(在 `rust/lithe-core` 下运行)
- `bun test src/features/file-search`(在 `windows/tauri` 下运行)
- `./scripts/agent-notes/verify-agent-notes.sh`

## 适用范围

- `rust/lithe-core/src/project/files.rs`
- `macos/Sources/LitheCoreContracts/Workspace/FileVisibilityRules.swift`
- `windows/tauri/src/features/file-search/lib/file-search-api.ts`
- `shared/contracts/rust-core-api.md`
5 changes: 5 additions & 0 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,11 @@ jobs:
shell: pwsh
run: node .agents/skills/write-stable-tests/scripts/run-bun-tests-with-timing.mjs --suite-timeout-ms 60000 --report .artifacts/test-stability/windows-quick-open-number-input.json -- src/features/quick-open/hooks/use-file-loader.test.tsx src/features/quick-open/hooks/use-file-search.test.tsx src/ui/number-input.test.tsx src/features/settings/components/editor-settings-routing.test.tsx

- name: Test workspace snapshot file-list request
shell: pwsh
# Isolated step: this suite installs a process-wide Tauri boundary mock.
run: node .agents/skills/write-stable-tests/scripts/run-bun-tests-with-timing.mjs --suite-timeout-ms 60000 --report .artifacts/test-stability/windows-file-search-snapshot.json -- src/features/file-search/lib/file-search-api.test.ts

- name: Test wheel scroll animation and container wheel ownership
shell: pwsh
timeout-minutes: 3
Expand Down
6 changes: 6 additions & 0 deletions macos/Resources/IDEAIcons/NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,9 @@ brand fallback: expui/nodes/dataTables{,_dark}.svg. These are unmodified
Community platform/icons/src assets from revision
c7f91397daa3a961b4e78bc634fe467a0a7d9ade (Apache-2.0).
The New menu Data Source entry reuses the Database dock dbms icon above.

File-tree icons for resource bundles and Office documents: fileTypes/i18n{,_dark}.svg
are unmodified platform/icons/src/expui/fileTypes assets, and fileTypes/custom{,_dark}.svg
is the unmodified platform/icons/src/fileTypes/custom.svg (IDEA's icon for its "Native"
file type, which has no separate dark variant), both from Community revision
fb72b4df43ab (Apache-2.0).
9 changes: 9 additions & 0 deletions macos/Resources/IDEAIcons/fileTypes/custom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions macos/Resources/IDEAIcons/fileTypes/custom_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions macos/Resources/IDEAIcons/fileTypes/i18n.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions macos/Resources/IDEAIcons/fileTypes/i18n_dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 17 additions & 3 deletions macos/Sources/Lithe/Theme/LitheIcons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ enum LitheIconKind: Hashable {
case readme
case license
case plainText
case i18n
case document
case image
case binary
case generic
Expand Down Expand Up @@ -116,6 +118,9 @@ enum LitheIcons {
.readme: "fileTypes/readme.svg",
.license: "fileTypes/license.svg",
.plainText: "fileTypes/text.svg",
.i18n: "fileTypes/i18n.svg",
// IDEA's "Native" file type (Office documents and similar) uses AllIcons.FileTypes.Custom.
.document: "fileTypes/custom.svg",
.image: "fileTypes/image.svg",
.binary: "fileTypes/archive.svg",
.generic: "fileTypes/unknown.svg",
Expand Down Expand Up @@ -197,6 +202,13 @@ enum LitheIcons {
ideaAssetPathsBySystemImage[systemImage]
}

/// Returns the imported IntelliJ asset backing a file-node kind. Unit
/// tests use this to verify catalog entries against the source resources
/// directory because the test host ships no app bundle.
static func ideaAssetPath(for kind: LitheIconKind) -> String? {
ideaAssetPaths[kind]
}

/// Returns the IntelliJ dark-theme sibling for an imported SVG path.
/// The caller still falls back to the base asset because not every
/// IntelliJ catalog entry ships a dedicated dark variant.
Expand Down Expand Up @@ -314,12 +326,14 @@ enum LitheIcons {
case "xml", "xsd", "wsdl", "pom": return .xml
case "properties", "ini", "cfg", "conf", "env": return .properties
case "yml", "yaml": return .yaml
case "json", "json5": return .json
case "json", "json5", "jsonl", "jsonlines", "ndjson", "ldjson": return .json
case "db", "sqlite", "sqlite3", "sql", "mv": return .database
case "md", "markdown", "rst", "adoc": return .markdown
case "txt", "log", "strings": return .plainText
case "txt", "log": return .plainText
case "strings": return .i18n
case "doc", "docx", "ppt", "pptx": return .document
case "csv", "tsv": return .csv
case "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "tiff": return .image
case "png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "tiff", "wbmp": return .image
case "zip", "gz", "tar", "so", "dylib", "dll", "icns", "pdf", "woff", "woff2", "ttf", "otf": return .binary
default: return .generic
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ package struct FileVisibilityRules: Hashable, Sendable {
/// VCS metadata (`.git`, `.svn`) is hidden like IDEA's default ignore
/// list; providers watch their own metadata directories.
package static let builtInHiddenDirectories = [
".git", ".svn", ".worktree", ".worktrees", ".build", ".swiftpm", "node_modules", "target", "build",
"DerivedData", ".gradle", ".next", "dist", "coverage", "design-qa-artifacts", ".artifacts"
".git", ".svn", ".worktree", ".worktrees", ".build", ".swiftpm", "node_modules", ".venv", "venv",
"__pycache__", "target", "build", "DerivedData", ".gradle", ".next", "dist", "coverage",
"design-qa-artifacts", ".artifacts"
]
/// Names and globs omitted from the project tree, search index, watchers,
/// and Local History. LSP-generated artifacts are not built-in; users can
/// add them once via the Hidden paths recommended-rules actions.
package static let builtInHiddenFilePatterns = [
".DS_Store",
"*.pyc",
".lithe/run/local.json",
]

Expand Down
40 changes: 40 additions & 0 deletions macos/Tests/LitheTests/FileTypeIconKindTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import AppKit
import Testing
@testable import Lithe

@MainActor
struct FileTypeIconKindTests {
@Test(arguments: ["a.jsonl", "a.jsonlines", "a.ndjson", "a.ldjson"])
func jsonLinesUseJsonIcon(fileName: String) {
#expect(LitheIcons.kind(forFilePath: "/fixture/\(fileName)") == .json)
}

@Test
func wbmpUsesImageIcon() {
#expect(LitheIcons.kind(forFilePath: "/fixture/a.wbmp") == .image)
}

@Test(arguments: ["a.doc", "a.docx", "a.ppt", "a.pptx"])
func officeDocumentsUseNativeFileIcon(fileName: String) {
#expect(LitheIcons.kind(forFilePath: "/fixture/\(fileName)") == .document)
}

@Test
func stringsFilesUseI18nIcon() {
#expect(LitheIcons.kind(forFilePath: "/fixture/Localizable.strings") == .i18n)
}

@Test(arguments: [LitheIconKind.document, .i18n])
func newKindsShipLightAndDarkAssets(kind: LitheIconKind) throws {
// The unit-test host ships no app bundle, so resolve the catalog entry
// and load the source assets directly instead of calling ideaImage.
let assetPath = try #require(LitheIcons.ideaAssetPath(for: kind))
let resources = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
.deletingLastPathComponent().deletingLastPathComponent().appendingPathComponent("Resources/IDEAIcons")
for path in [assetPath, LitheIcons.darkIdeaAssetPath(for: assetPath)] {
let image = try #require(NSImage(contentsOf: resources.appendingPathComponent(path)),
"Missing IDEA icon asset: \(path)")
#expect(image.size.width > 0)
}
}
}
2 changes: 1 addition & 1 deletion macos/Tests/LitheTests/GitCommitTreeStyleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ struct GitCommitTreeStyleTests {
#expect(LitheIcons.kind(forFilePath: "ChangesSidebarView.swift") == .swiftSource)
#expect(LitheIcons.kind(forFilePath: "git-local-changelists.json") == .json)
#expect(LitheIcons.kind(forFilePath: "CHANGELOG.md") == .markdown)
#expect(LitheIcons.kind(forFilePath: "Localizable.strings") == .plainText)
#expect(LitheIcons.kind(forFilePath: "Localizable.strings") == .i18n)
}
}
Loading
Loading