diff --git a/docs/windows-idea-ui-audit.md b/docs/windows-idea-ui-audit.md new file mode 100644 index 000000000..b086ac5ce --- /dev/null +++ b/docs/windows-idea-ui-audit.md @@ -0,0 +1,69 @@ +# Windows UI 打磨审计 + +> 基线:`520f3c77 feat(windows): make IDEA icons the default file theme`。 +> 日期:2026-08-20。 +> 用途:记录正式打磨的基础与校正结果。Windows IDEA 2025.2.1 是对应功能的主视觉参考;Mac Lithe 仅借鉴成熟细节;不做整窗复刻。 + +## 1. 已完成的基础 + +### 默认文件图标主题 + +- `defaultSettings.iconTheme` 已是 `idea-icons`(`windows/tauri/src/features/settings/config/default-settings.ts`)。 +- 旧 Lithe 图标主题 ID 会迁移到 `idea-icons`;旧 Material/Seti 别名按现有规则归一化(`windows/tauri/src/features/settings/lib/settings-normalization.ts`)。 +- bundled 注册保留 IDEA、Material、Pierre、Symbols,不再注册旧 Lithe 图标主题(`windows/tauri/src/extensions/bundled/bundled-extension-manifests.ts`)。 +- 对应默认值、迁移和 manifest 注册已有测试覆盖。 +- foundation 提交时已记录通过 186 个前端测试、typecheck、production build、Windows boundary、Release build 和 69 个 Tauri Rust tests;正式打磨后的结果必须重新验证,不能沿用这组结论。 + +### IDEA 资产通路 + +- 文件图标 manifest 已包含常用扩展名、文件名、通用文件和文件夹的明暗 IDEA SVG(`windows/tauri/src/extensions/bundled/icon-themes/idea/extension.json`)。 +- bundled 资产解析器已能加载 IDEA 图标主题(`windows/tauri/src/extensions/icon-themes/bundled-icon-theme-assets.ts`)。 +- 通用操作图标已有生成资产表与统一 `icons.tsx` 入口,能够在明暗主题间选择 IDEA 变体,并保留 Lucide fallback(`windows/tauri/src/ui/icons/idea-assets.generated.ts`、`windows/tauri/src/ui/icons.tsx`)。 +- Maven 项目图标(`pom.xml`)的生成器 source 位于 intellij-community 的 `plugins/maven/...` 目录(`idea-file-icon-mappings.json` 的 source/asset 分离机制);本地稀疏克隆需包含该目录才能跑生成器 `--check` 复现校验。 + +### IDEA 风格基础色板 + +- `lithe-dark` 已采用 `#1e1f22` 背景、`#2b2d30` 表面、`#43454a` 边框、`#2e436e` 选中和 `#3574f0` 主色等 IDEA 风格令牌;亮色变体也已有对应层级(`windows/tauri/src/extensions/themes/builtin/lithe.json`)。 +- `lithe-dark` 语法色已整体换成 New Darcula 系(橙关键字/青数字/绿字符串等),编辑器 selection `#214283`、光标 `#ced0d6`;Monaco 的行高亮与失焦选区已与选中色解耦(`monaco/theme.ts`)。 +- `theme.css` 已通过语义变量向组件提供 background、surface、border、selected、primary 等颜色,后续应继续走令牌,不在组件散落 raw hex。 + +## 2. 当前主要差距 + +| 区域 | 当前状态 | 本轮动作 | +| --- | --- | --- | +| 图标一致性 | IDEA 文件主题、Java 声明节点、标准 Maven root/package 和操作资产已接入;文件树与 editor tab 共用 Java 语义,不能确认时仍回退通用图标 | 保持可见行/已打开 buffer 的浅词法与真实 `pom.xml` 约束;不按文件名猜类型,不建立全量索引 | +| Project 树 | 已有虚拟化、Compact Folders、git 状态和主题图标入口;source root 会截断 compact,package 仅改变显示名与图标;层级 guide 静止透明、靠近或键盘聚焦时显现 | 保持 row-model 投影与 guide 原命中几何;selection、rename、watcher、路径操作继续使用真实路径 | +| 主工具栏 | Project、branch、Quick Open、更新和窗口控制均有真实 action;中文紧凑菜单已禁止收缩换行 | 只调整视觉分组;不放禁用运行选择器或重复项目入口 | +| Project tabs | 完整切换/关闭/拖拽逻辑存在,单项目已隐藏 | 多项目保留独立紧凑行,避免重写状态或强塞主工具栏 | +| Workbench | `lithe-glass-island`、resize 和透明逻辑是成熟结构;Project/editor 统一使用 `background` token,并保留中间左右边线、真实 4px gutter 与 resize handle;主工作区顶/底外沿不描线;editor/BottomPane 四角使用同一半径,与右侧 rail 无 gap、无相邻双边线 | 保留轻圆角容器、内容宽度和 resize 行为,只校准 gap、分隔、圆角与表面层级 | +| Editor tabs | 交互能力较多,视觉上仍偏厚、图标与关闭态不够统一 | 只改高度、激活线、hover、图标槽和边框,完整保留 tab 行为 | +| Activity rail | 左 rail 承担真实路由;右侧恢复 38px Extensions rail,并与左 rail 共用 `surface` 颜色;唯一 Puzzle 按钮打开真实 singleton Extensions buffer,active 跟随当前 buffer | 只显示已有真实 action;预留空白不等于预造未来按钮,保持紧凑中性选中态 | +| Bottom pane | Terminal、Run、Git Log、Debugger、Buffers 共用容器;Terminal 最后一个 session 关闭时会自动收起,其他工具窗不受影响 | 只识别同 workspace 的 terminal 数量 `>0 → 0` 且当前可见 tab 为 Terminal;全局新建入口必须重新展开并仅创建一次 | +| Tool window header | 各 feature 的标题和动作排布不完全一致,Project 区的层级感不足 | 建立或复用统一 presentation 原语,不改变各 feature action | +| Status bar | 已组件化且支持排序,但文字、chip、图标和间距不在同一密度体系 | 统一度量并重点检查 zh-CN 长文案和窄窗口容量 | +| 字体与语言 | 新默认已为 Microsoft YaHei UI 13 与 `zh-CN`;仅成对迁移旧 Geist Sans 15 默认值 | 保留用户显式选择,验证中文、大字号和 Windows 125% 缩放 | + +## 3. 明确不做 + +- 不复刻 IDEA 2025.2.1 的全部布局、交互或 Ultimate 专有功能。 +- 不建立完整 Java AST、Maven/Gradle Project Model、持久化语义缓存、后台全树索引或 sidecar;仅允许有界的可见行浅词法识别和已加载标准 Maven 路径投影。 +- 不修改 Tauri 命令、platform dispatcher、shared contracts、Rust Core 或 macOS。 +- 不新增字体或 SVG 转换依赖来解决现有 CSS/资产通路已经能完成的问题。 +- 不要求 2px 像素误差、透明叠图、固定九张证据图或五次启动统计。 +- 不把本机的 `intellij-community`、示例项目或截图绝对路径写入产品代码。 + +## 4. 风险与护栏 + +- **全局令牌影响所有主题。** `theme.css` 中的结构变量可以共享,IDEA 专属颜色必须留在 Lithe 主题;每轮至少 smoke 一个非 Lithe 暗色主题和一个亮色主题。 +- **UI 密度会暴露中文容量问题。** 主工具栏、Project tabs、状态栏和设置弹窗需在常规/较窄窗口及 125% 缩放下检查;树行随用户字号增长,不能为了紧凑裁字。 +- **图标同名不等于同义。** 返回/前进、上下移动、展开/收起、运行/调试和关闭类资产需要逐个看 SVG;不能让 `mirrored` 再次反转已有方向。 +- **presentation 重排容易误伤行为。** 项目切换、tab DnD/固定/预览、rail 路由、pane resize 和窗口拖动依赖现有组件;样式改动前后用现有测试与人工清单对照。 +- **点击区不能随视觉尺寸一起缩没。** 图标可以更紧凑,交互 hitbox、键盘焦点和可访问名称必须保留。 + +## 5. 完成判据 + +- 用户认可中文 Dark 主窗口的整体质感,并认为已达到“参考 IDEA New UI 打磨后的 Lithe”,无需与 IDEA 像素一致。 +- 主工具栏、Project 树、editor tabs、activity rail、tool-window header 和 status bar 形成同一套视觉语言。 +- 文件图标和常用操作图标语义正确,IDEA Icons 默认生效,其他显式图标主题仍可使用。 +- 中文常规/窄窗口不破版,Light、English 和第三方主题完成 smoke。 +- `bun test`、typecheck、production build、Windows boundary、Release build 和 Tauri Rust tests 均通过;未执行项如实披露。 diff --git a/docs/windows-idea-ui-polish-plan.md b/docs/windows-idea-ui-polish-plan.md new file mode 100644 index 000000000..2469e4c01 --- /dev/null +++ b/docs/windows-idea-ui-polish-plan.md @@ -0,0 +1,91 @@ +# Windows 端 IDEA New UI 风格打磨方案 + +> 状态:已获准正式开发,2026-08-20 按“现有功能逐项效仿”口径校正。 +> 参考:Windows IntelliJ IDEA 2025.2.1 是主视觉标准;Mac Lithe 截图只借鉴已经验证成熟的局部呈现;本地 `intellij-community` 提供主题与 SVG 资产。它们不是整窗克隆规范。 + +## 1. 目标与边界 + +- 只打磨 Windows React/Tauri 产品,macOS 不动。 +- 中文界面优先,Dark 先完成;Light 与 English 只做不破版检查。 +- 目标是让 Lithe 的主窗口明显具备 IDEA New UI 的图标语言、表面层级、密度和控件质感,同时保留 Lithe 的名称、Logo、多项目能力和已有功能入口。 +- 允许调整 `windows/tauri/src/features/**/components`、对应样式以及 `windows/tauri/src/ui` 中的呈现壳和可复用原语。 +- 功能、状态模型、数据流、持久化和现有交互不变;不改 `platform` 调度、Tauri 命令、`shared` 契约、Rust Core 或 macOS 实现。 +- 先确认 Windows 已有真实功能,再寻找 Windows IDEA 对应 UI;没有功能就不显示按钮、占位入口或空工具窗。Mac 端仅在相同功能已有更成熟落法时作为补充参考。 +- 不为图标显示新增完整 Java 解析器、持久化语义索引、sidecar 或 Project Model。IDEA Icons 可对虚拟列表中实际显示的 Java 文件及已经打开的 editor buffer 做有界浅词法识别,并从已加载且含 `pom.xml` 的标准 Maven 目录结构投影 source root/package;声明名不匹配、读取失败或结构不标准时回退通用图标。 +- 优先复用现有依赖和图标漏斗。只有现有栈确实无法完成时才考虑新依赖,并单独说明必要性与体积影响。 + +## 2. 视觉参考与资产规则 + +- Windows IDEA 2025.2.1 截图用于对应功能的观感对照:工具栏层级、Project 树密度、编辑器标签、侧边工具窗、分隔线、选中态和状态栏。Mac Lithe 不决定 Windows 布局,只提供可选的产品细节。 +- `intellij-community` 用于查找开源的 IDEA SVG、明暗变体和主题色。不得在运行时代码中写入本机仓库路径,也不照搬 Swing/Kotlin 实现。 +- SVG 进入仓库时保留上游文件内已有的版权信息,通过现有 IDEA 资产生成器和 `icons.tsx` 漏斗接入;不用同名猜语义,方向图标要核对前进/后退、展开/收起和 `mirrored` 约定。 +- IDEA Icons 作为默认文件图标主题;Material、Pierre、Symbols 等显式选择继续保留。 +- 颜色、尺寸和状态值集中在主题令牌或共享 UI 原语中。业务组件不得散落 IDEA 色值和重复魔法数字。 +- 主题决定颜色,结构样式决定布局。调整结构令牌时必须检查其他 bundled 主题,不能让第三方配色主题意外继承 IDEA 专属颜色。 + +## 3. 实施顺序 + +### A. 图标与 Project 树 + +- 补齐主工具栏、常用操作、activity rail、Project 树和编辑器标签实际用到的 IDEA SVG 映射。 +- 统一 16px 图标槽、展开箭头、目录开合、文件类型、悬停与选中状态。 +- 调整 Project 树的行高、缩进、文字基线、选中背景、工具窗标题和右侧操作按钮。 +- 保留 Project 树层级 guide 的原有命中区与折叠行为;静止时保持透明,鼠标靠近 guide 区或键盘 `focus-visible` 位于树内时淡入,不允许为显线改变缩进几何。 +- Java `class/interface/enum/@interface/record` 只在浅词法结果与文件 basename 严格一致时使用对应图标;Exception 只认显式直接继承 Java 内建异常基类。磁盘只读取虚拟列表实际挂载的行,editor tab 只消费已打开 buffer,不扫描全树;不能确认时保持 Java 通用图标。 +- 标准 Maven `src/main/java`、`src/test/java`、resources roots 及合法 package 路径由已加载树中的真实 `pom.xml` 约束;Compact Folders 不得跨 source root 合并,selection、rename、watcher 和文件操作继续使用真实路径。 + +### B. 主题令牌与通用原语 + +- 继续使用已经落地的 IDEA 风格暗色色板,校准背景/表面层级、边框、分隔线、悬停、选中和焦点态。 +- Project 文件区与 editor 使用同一个 `background` 主题令牌;两者之间保留 token border 与 workbench gap,主工作区顶/底外沿不额外描线;editor 与右侧 Extensions rail 直接衔接,不用双边框或空隙制造层级。 +- 统一工具栏按钮、图标按钮、tabs、列表行、弹层和 resize 分隔条的高度、圆角及视觉 padding。 +- 点击热区可以大于图标本身,但不能靠额外可见 padding 把界面重新撑松。 +- 字体以 Windows 中文清晰度和可靠 fallback 为先,不为追求英文截图外观盲目增加字体包。 + +### C. 主窗口 chrome + +- 主工具栏:只放真实的菜单、Project、branch、Quick Open、更新和窗口控制;不复制不存在的运行选择器或重复项目入口。 +- Project tabs:这是 Lithe 的真实多项目能力;单项目隐藏,多项目保留独立紧凑行,不为了像 IDEA 强塞进主工具栏。 +- Workbench:保留现有可调整大小的轻圆角面板结构;Project 与 editor 之间继续使用左右 token border 和既有 workbench gap,resize handle 留在 gap 内;主工作区顶/底外沿不绘制横线。editor 与 BottomPane 使用完整 `rounded-xl`,右上/右下与左侧采用同一半径;它们与右侧 Extensions rail 直接相邻,不画相邻边线,也不在两者之间留 gap。只收紧外缝、层级和分隔,不为追求截图拆掉成熟容器或重排业务挂载点。 +- Editor tabs:统一高度、激活下划线、文件图标、关闭按钮和 hover 状态;拖拽、固定、预览、脏标记、键盘和跨窗行为不变。 +- Activity rail / tool-window header:左 rail 保持现有真实路由;右侧固定预留 38px Extensions rail,并与左 rail 使用同一个 `surface` 颜色,目前只放一个能打开真实 singleton Extensions 页面的 Puzzle 按钮。未来只有存在真实 action 时才增加按钮。统一尺寸、选中反馈、标题与现有动作排列。 +- Status bar:统一高度、间距、文字层级与图标,检查中文长文案和项目状态容量,避免同一状态在顶部与底部无意义重复。 +- Bottom pane:关闭最后一个 terminal 后自动收起 Terminal pane;重新从 activity rail、菜单或命令面板新建时再展开且只创建一个。Run、Git Log、Debugger 与 Buffers 不受 terminal 归零影响。 + +## 4. 开发纪律 + +- 一个总 PR,按“图标与文件树 → 令牌/原语 → 顶部 chrome/tabs → rail/tool window/status bar → 集成收口”分层提交。 +- 每层提交保持应用可运行;保留相关行为测试,不用大范围快照替代功能断言。 +- 呈现组件继续调用现有 feature actions/stores,不从 view 直接调用 Tauri API,不新增跨层快捷通道。 +- 不做无关重构、依赖升级或生成物提交;不硬编码本机项目、工具安装或截图路径。 +- 若某项“更像 IDEA”的改法会改变用户行为或跨越边界,先停在现状,不在 UI 打磨中顺手重设计。 +- capability 不可用或没有渲染消费者的入口必须隐藏;不得用永久 disabled 控件表达未来功能。 + +## 5. 验收方式 + +视觉验收以中文 Dark 主窗口为主,不设逐像素误差或固定截图数量。使用同一个真实项目打开文件树和若干编辑器标签,与 IDEA 参考图并排查看: + +- 第一眼能识别为同一类 New UI 视觉语言,且仍然是 Lithe。 +- 工具栏、Project 树、editor tabs、activity rail、tool-window header 和 status bar 的密度、层级、图标风格一致。 +- 常规窗口与较窄窗口无重叠、无不可达操作、无中文截断;125% Windows 缩放下显示正常。 +- Dark 为完整验收;Light、主题切换和 English 各做一次 smoke;Material、Pierre、Symbols 仍可选择。 +- 人工回归项目切换/溢出、文件树展开与选中、tab 打开/关闭/重排/固定、rail 切换、pane resize、窗口拖动和状态栏显示。 +- 最终效果由用户查看实机截图或运行版本后确认。 + +## 6. 验证矩阵 + +迭代时运行最小相关测试、`bun test` 和 `bun run typecheck`。总 PR 收口前在 Windows 完成: + +```powershell +Set-Location windows/tauri +bun test +bun run typecheck +bun run build + +Set-Location ../.. +./scripts/verify-windows-boundaries.ps1 +./scripts/build-windows.ps1 -Configuration Release +cargo test --manifest-path windows/tauri/src-tauri/Cargo.toml +``` + +任何未实际执行的检查都必须在交付说明中明确列出,不能按通过报告。 diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 index b427cddd3..72ab82883 100644 --- a/scripts/build-windows.ps1 +++ b/scripts/build-windows.ps1 @@ -8,8 +8,17 @@ param( $ErrorActionPreference = "Stop" $root = Split-Path -Parent $PSScriptRoot $windowsApp = Join-Path $root "windows/tauri" +$bundledExtensionsSource = [System.IO.Path]::GetFullPath((Join-Path $windowsApp "src/extensions/bundled")) +$preparedJdtlsRoot = $null +if (-not (Test-Path -LiteralPath $bundledExtensionsSource -PathType Container)) { + throw "Bundled extensions source directory is missing: $bundledExtensionsSource" +} if ($Configuration -eq "Release") { - & (Join-Path $root "scripts/prepare-jdtls.ps1") | Out-Null + $prepareOutput = @(& (Join-Path $root "scripts/prepare-jdtls.ps1")) + if ($prepareOutput.Count -eq 0) { + throw "JDTLS preparation did not return an output directory." + } + $preparedJdtlsRoot = [System.IO.Path]::GetFullPath([string]$prepareOutput[-1]) } Set-Location $windowsApp @@ -40,4 +49,53 @@ if ($Configuration -eq "Debug") { & bunx @tauriArgs if ($LASTEXITCODE -ne 0) { throw "Windows Tauri build failed" } +$profileName = if ($Configuration -eq "Debug") { "debug" } else { "release" } +$cargoTargetRoot = [System.IO.Path]::GetFullPath((Join-Path $windowsApp "src-tauri/target")) +$profileRoot = [System.IO.Path]::GetFullPath( + (Join-Path (Join-Path $cargoTargetRoot $RustTarget) $profileName) +) +$trimCharacters = [char[]]@([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) +$targetPrefix = $cargoTargetRoot.TrimEnd($trimCharacters) + [System.IO.Path]::DirectorySeparatorChar +if (-not $profileRoot.StartsWith($targetPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Windows Cargo target profile must stay inside $cargoTargetRoot" +} +if (-not (Test-Path -LiteralPath (Join-Path $profileRoot "lithe-windows.exe") -PathType Leaf)) { + throw "Windows Tauri executable is missing from target profile: $profileRoot" +} + +function Copy-ResourceDirectory { + param([string]$Source, [string]$RelativeDestination) + + if (-not (Test-Path -LiteralPath $Source -PathType Container)) { + throw "Bundled resource source directory is missing: $Source" + } + $destination = [System.IO.Path]::GetFullPath((Join-Path $profileRoot $RelativeDestination)) + $profilePrefix = $profileRoot.TrimEnd($trimCharacters) + [System.IO.Path]::DirectorySeparatorChar + if (-not $destination.StartsWith($profilePrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Bundled resource destination must stay inside target profile: $destination" + } + if (Test-Path -LiteralPath $destination) { + Remove-Item -Recurse -Force -LiteralPath $destination + } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $destination) | Out-Null + Copy-Item -Recurse -Force -LiteralPath $Source -Destination $destination + $destination +} + +$bundledExtensionsDestination = Copy-ResourceDirectory $bundledExtensionsSource "extensions/bundled" +foreach ($relativePath in @("icon-themes", "themes", "icon-themes/idea/extension.json")) { + if (-not (Test-Path -LiteralPath (Join-Path $bundledExtensionsDestination $relativePath))) { + throw "Bundled extensions staging is incomplete: $relativePath" + } +} + +if ($Configuration -eq "Release") { + $jdtlsDestination = Copy-ResourceDirectory $preparedJdtlsRoot "LanguageServers/jdtls" + foreach ($relativePath in @("bin/jdtls.bat", "config_win", "plugins")) { + if (-not (Test-Path -LiteralPath (Join-Path $jdtlsDestination $relativePath))) { + throw "Bundled JDTLS staging is incomplete: $relativePath" + } + } +} + Write-Output "Windows Tauri build completed." diff --git a/windows/tauri/scripts/generate-idea-file-icons.ts b/windows/tauri/scripts/generate-idea-file-icons.ts new file mode 100644 index 000000000..1789c83cd --- /dev/null +++ b/windows/tauri/scripts/generate-idea-file-icons.ts @@ -0,0 +1,228 @@ +/** + * Builds the bundled `idea-icons` file icon theme from IntelliJ platform + * assets: copies SVGs into + * src/extensions/bundled/icon-themes/idea/icons and emits the theme's + * extension.json manifest. + * + * Usage (run from windows/tauri): + * bun scripts/generate-idea-file-icons.ts --source /platform/icons/src [--check] + * + * Dark appearance uses the `_dark.svg` sibling when present; the light + * appearance always uses the base file. Copied SVGs keep their embedded + * Apache-2.0 copyright comments untouched. + */ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +interface IconMappingSpec { + source: string; + asset: string; + definition?: string; + darkSource?: string; + darkAsset?: string; +} + +type IconMapping = string | IconMappingSpec; + +interface FileThemeMapping { + fileExtensions: Record; + filenames: Record; + folders: Record; + defaultFile: IconMapping; + defaultFolder: IconMapping; +} + +interface ResolvedIconMapping { + source: string; + asset: string; + definition: string; + darkSource: string; + darkAsset: string; +} + +const REPO_ROOT = resolve(import.meta.dir, ".."); +const MAPPING_PATH = join(import.meta.dir, "idea-file-icon-mappings.json"); +const THEME_DIR = join(REPO_ROOT, "src/extensions/bundled/icon-themes/idea"); +const MANIFEST_PATH = join(THEME_DIR, "extension.json"); + +function parseArgs(): { source: string | null; check: boolean } { + const argv = process.argv.slice(2); + let source: string | null = null; + let check = false; + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === "--source" && argv[i + 1]) { + source = resolve(argv[i + 1]); + i += 1; + } else if (argv[i] === "--check") { + check = true; + } + } + return { source, check }; +} + +function sha(buffer: Buffer): string { + return createHash("sha256").update(buffer).digest("hex"); +} + +function definitionName(asset: string): string { + const file = asset.split("/").pop() ?? asset; + return `idea-${file.replace(/\.svg$/, "")}`; +} + +function resolveIconMapping(mapping: IconMapping): ResolvedIconMapping { + const spec = typeof mapping === "string" ? { source: mapping, asset: mapping } : mapping; + if (spec.asset.startsWith("/") || spec.asset.split("/").includes("..")) { + throw new Error(`Icon asset path must stay inside the bundled theme: ${spec.asset}`); + } + + return { + source: spec.source, + asset: spec.asset, + definition: spec.definition ?? definitionName(spec.asset), + darkSource: spec.darkSource ?? spec.source.replace(/\.svg$/, "_dark.svg"), + darkAsset: spec.darkAsset ?? spec.asset.replace(/\.svg$/, "_dark.svg"), + }; +} + +function sourcePath(source: string): string { + return resolve(sourceRoot!, source); +} + +function themeAssetPath(mapping: ResolvedIconMapping, dark: boolean): string { + return `./icons/${dark ? mapping.darkAsset : mapping.asset}`; +} + +const { source: sourceRoot, check } = parseArgs(); +if (!sourceRoot) { + console.error("Missing --source /platform/icons/src"); + process.exit(1); +} + +const mapping = JSON.parse(readFileSync(MAPPING_PATH, "utf8")) as FileThemeMapping; + +const entries = new Map(); // definition name -> source/asset pair +const mapKeys = new Map(); // ".ts" / "Dockerfile" / "src" -> definition name + +function registerEntries(record: Record): string[] { + const keys: string[] = []; + for (const [key, rawMapping] of Object.entries(record)) { + const resolvedMapping = resolveIconMapping(rawMapping); + entries.set(resolvedMapping.definition, resolvedMapping); + mapKeys.set(key, resolvedMapping.definition); + keys.push(key); + } + return keys; +} + +const extensionKeys = registerEntries(mapping.fileExtensions); +const filenameKeys = registerEntries(mapping.filenames); +const folderKeys = registerEntries(mapping.folders); +const defaultFile = resolveIconMapping(mapping.defaultFile); +const defaultFolder = resolveIconMapping(mapping.defaultFolder); +entries.set(defaultFile.definition, defaultFile); +entries.set(defaultFolder.definition, defaultFolder); + +const missing: string[] = []; +const darkVariants = new Map(); // definition name -> has dark pair +for (const [name, iconMapping] of entries) { + if (!existsSync(sourcePath(iconMapping.source))) { + missing.push(iconMapping.source); + continue; + } + const hasDark = existsSync(sourcePath(iconMapping.darkSource)); + darkVariants.set(name, hasDark); +} +if (missing.length > 0) { + console.error(missing.map((s) => `source SVG not found: ${s}`).join("\n")); + process.exit(1); +} + +const iconDefinitions: Record = {}; +const lightIconDefinitions: Record = {}; +const fileExtensions: Record = {}; +const filenames: Record = {}; +const folders: Record = {}; +const expandedFolders: Record = {}; + +for (const [name, iconMapping] of entries) { + const hasDark = darkVariants.get(name) === true; + iconDefinitions[name] = themeAssetPath(iconMapping, hasDark); + lightIconDefinitions[name] = themeAssetPath(iconMapping, false); +} +for (const key of extensionKeys) fileExtensions[key] = mapKeys.get(key)!; +for (const key of filenameKeys) filenames[key] = mapKeys.get(key)!; +for (const key of folderKeys) { + folders[key] = mapKeys.get(key)!; + expandedFolders[key] = mapKeys.get(key)!; +} + +const manifest = { + $schema: "https://lithe.dev/schemas/extension.json", + id: "lithe.icon-theme.idea-icons", + name: "idea-icons", + displayName: "IDEA Icons", + publisher: "Lithe", + categories: ["Icon Theme"], + version: "0.1.0", + description: + "File icons from the IntelliJ platform icon set (intellij-community, Apache-2.0, embedded per-file copyright notices preserved).", + icons: [ + { + id: "idea-icons", + name: "IDEA Icons", + description: + "File icons from the IntelliJ platform icon set (intellij-community, Apache-2.0).", + defaultFile: defaultFile.definition, + defaultFolder: defaultFolder.definition, + defaultFolderOpen: defaultFolder.definition, + iconDefinitions, + lightIconDefinitions, + fileExtensions, + filenames, + folders, + expandedFolders, + }, + ], +}; + +const filesToCopy: Array<{ source: string; asset: string }> = []; +for (const [name, iconMapping] of entries) { + filesToCopy.push({ source: iconMapping.source, asset: iconMapping.asset }); + if (darkVariants.get(name) === true) { + filesToCopy.push({ source: iconMapping.darkSource, asset: iconMapping.darkAsset }); + } +} + +if (check) { + const problems: string[] = []; + const current = readFileSync(MANIFEST_PATH, "utf8"); + const expected = JSON.stringify(manifest, null, 2) + "\n"; + if (current !== expected) { + problems.push("extension.json is out of date; rerun the generator"); + } + for (const file of filesToCopy) { + const copied = join(THEME_DIR, "icons", file.asset); + if (!existsSync(copied)) { + problems.push(`missing copied asset: idea/icons/${file.asset}`); + } else if (sha(readFileSync(copied)) !== sha(readFileSync(sourcePath(file.source)))) { + problems.push(`copied asset differs from source: idea/icons/${file.asset}`); + } + } + if (problems.length > 0) { + console.error(problems.join("\n")); + process.exit(1); + } + console.log(`idea-icons theme check passed (${entries.size} definitions)`); + process.exit(0); +} + +for (const file of filesToCopy) { + const dest = join(THEME_DIR, "icons", file.asset); + mkdirSync(dirname(dest), { recursive: true }); + writeFileSync(dest, readFileSync(sourcePath(file.source))); +} +writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n"); +console.log( + `Copied ${filesToCopy.length} SVGs and emitted idea-icons extension.json (${entries.size} definitions)`, +); diff --git a/windows/tauri/scripts/generate-idea-icons.ts b/windows/tauri/scripts/generate-idea-icons.ts new file mode 100644 index 000000000..b04ed0434 --- /dev/null +++ b/windows/tauri/scripts/generate-idea-icons.ts @@ -0,0 +1,171 @@ +/** + * Copies IntelliJ platform icon SVGs into the Lithe icon funnel and emits the + * generated asset map consumed by `src/ui/icons.tsx`. + * + * Usage (run from windows/tauri): + * bun scripts/generate-idea-icons.ts --source /platform/icons/src [--check] + * + * The mapping file (scripts/idea-icon-mappings.json) is the single source of + * truth for which Lithe icon exports use IntelliJ assets; every other export + * keeps its lucide fallback. Source paths are repo-relative and resolved + * against --source, so no machine-specific path is persisted. Each copied SVG + * keeps its embedded Apache-2.0 copyright comment untouched. + */ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +interface MappingEntry { + icon: string; + source: string | null; + note?: string; +} + +interface MappingFile { + icons: MappingEntry[]; +} + +const REPO_ROOT = resolve(import.meta.dir, ".."); +const MAPPING_PATH = join(import.meta.dir, "idea-icon-mappings.json"); +const ICONS_TSX_PATH = join(REPO_ROOT, "src/ui/icons.tsx"); +const ASSETS_DIR = join(REPO_ROOT, "src/ui/icons/idea"); +const GENERATED_TS_PATH = join(REPO_ROOT, "src/ui/icons/idea-assets.generated.ts"); + +function parseArgs(): { source: string | null; check: boolean } { + const argv = process.argv.slice(2); + let source: string | null = null; + let check = false; + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === "--source" && argv[i + 1]) { + source = resolve(argv[i + 1]); + i += 1; + } else if (argv[i] === "--check") { + check = true; + } + } + return { source, check }; +} + +function toIdentifier(icon: string): string { + return icon.replace(/Icon$/, "").replace(/^./, (c) => c.toLowerCase()); +} + +function sha(buffer: Buffer): string { + return createHash("sha256").update(buffer).digest("hex"); +} + +const { source: sourceRoot, check } = parseArgs(); +if (!sourceRoot) { + console.error("Missing --source /platform/icons/src"); + process.exit(1); +} + +const mapping = JSON.parse(readFileSync(MAPPING_PATH, "utf8")) as MappingFile; +const iconsTsx = readFileSync(ICONS_TSX_PATH, "utf8"); + +interface PlannedIcon { + icon: string; + source: string; + light: string; + dark: string | null; +} + +const planned: PlannedIcon[] = []; +const missingSources: string[] = []; +const unknownExports: string[] = []; +const duplicateIcons: string[] = []; + +for (const entry of mapping.icons) { + if (planned.some((p) => p.icon === entry.icon)) { + duplicateIcons.push(entry.icon); + continue; + } + if (!new RegExp(`export const ${entry.icon} =`).test(iconsTsx)) { + unknownExports.push(entry.icon); + continue; + } + if (!entry.source) { + continue; + } + const lightAbs = join(sourceRoot, entry.source); + if (!existsSync(lightAbs)) { + missingSources.push(entry.source); + continue; + } + const darkRel = entry.source.replace(/\.svg$/, "_dark.svg"); + const darkAbs = join(sourceRoot, darkRel); + planned.push({ + icon: entry.icon, + source: entry.source, + light: entry.source, + dark: existsSync(darkAbs) ? darkRel : null, + }); +} + +planned.sort((a, b) => a.icon.localeCompare(b.icon)); + +const failures = [ + ...missingSources.map((s) => `source SVG not found: ${s}`), + ...unknownExports.map((i) => `not exported from src/ui/icons.tsx: ${i}`), + ...duplicateIcons.map((i) => `duplicate mapping entry: ${i}`), +]; +if (failures.length > 0) { + console.error(failures.join("\n")); + process.exit(1); +} + +let generated = `// AUTO-GENERATED by scripts/generate-idea-icons.ts — do not edit by hand.\n`; +generated += `// Mapping source: scripts/idea-icon-mappings.json (paths relative to the\n`; +generated += `// intellij-community platform/icons/src checkout passed via --source).\n`; +generated += `// Keys are icon export display names resolved by createIconComponent.\n\n`; + +for (const p of planned) { + generated += `import ${toIdentifier(p.icon)}Light from "./idea/${p.light}?url";\n`; + if (p.dark) { + generated += `import ${toIdentifier(p.icon)}Dark from "./idea/${p.dark}?url";\n`; + } +} +generated += `\nexport interface IdeaIconAsset {\n light: string;\n dark: string;\n}\n\n`; +generated += `export const ideaIconAssets: Record = {\n`; +for (const p of planned) { + const darkId = p.dark ? `${toIdentifier(p.icon)}Dark` : `${toIdentifier(p.icon)}Light`; + generated += ` ${p.icon}: { light: ${toIdentifier(p.icon)}Light, dark: ${darkId} },\n`; +} +generated += `};\n`; + +if (check) { + const problems: string[] = []; + if (readFileSync(GENERATED_TS_PATH, "utf8") !== generated) { + problems.push("src/ui/icons/idea-assets.generated.ts is out of date; rerun the generator"); + } + for (const p of planned) { + const pairs = p.dark ? [p.light, p.dark] : [p.light]; + for (const rel of pairs) { + const copied = join(ASSETS_DIR, rel); + if (!existsSync(copied)) { + problems.push(`missing copied asset: src/ui/icons/idea/${rel}`); + } else if (sha(readFileSync(copied)) !== sha(readFileSync(join(sourceRoot, rel)))) { + problems.push(`copied asset differs from source: src/ui/icons/idea/${rel}`); + } + } + } + if (problems.length > 0) { + console.error(problems.join("\n")); + process.exit(1); + } + console.log(`idea-icons check passed (${planned.length} mapped icons)`); + process.exit(0); +} + +for (const p of planned) { + const pairs = p.dark ? [p.light, p.dark] : [p.light]; + for (const rel of pairs) { + const dest = join(ASSETS_DIR, rel); + mkdirSync(dirname(dest), { recursive: true }); + writeFileSync(dest, readFileSync(join(sourceRoot, rel))); + } +} +writeFileSync(GENERATED_TS_PATH, generated); +console.log( + `Copied ${planned.reduce((n, p) => n + (p.dark ? 2 : 1), 0)} SVGs and emitted idea-assets.generated.ts (${planned.length} icons mapped)`, +); diff --git a/windows/tauri/scripts/idea-file-icon-mappings.json b/windows/tauri/scripts/idea-file-icon-mappings.json new file mode 100644 index 000000000..2bc50aed9 --- /dev/null +++ b/windows/tauri/scripts/idea-file-icon-mappings.json @@ -0,0 +1,111 @@ +{ + "description": "Lithe idea-icons file theme mapping: file extension / filename -> IntelliJ platform icon SVG (relative to intellij-community platform/icons/src). Managed by scripts/generate-idea-file-icons.ts. Assets without a dark pair render the base file in both appearances.", + "fileExtensions": { + ".ts": "expui/fileTypes/typeScript.svg", + ".mts": "expui/fileTypes/typeScript.svg", + ".cts": "expui/fileTypes/typeScript.svg", + ".tsx": "expui/fileTypes/typeScript.svg", + ".js": "expui/fileTypes/javaScript.svg", + ".mjs": "expui/fileTypes/javaScript.svg", + ".cjs": "expui/fileTypes/javaScript.svg", + ".jsx": "expui/fileTypes/javaScript.svg", + ".json": "expui/fileTypes/json.svg", + ".jsonc": "expui/fileTypes/json.svg", + ".json5": "expui/fileTypes/json.svg", + ".md": "expui/fileTypes/markdown.svg", + ".mdx": "expui/fileTypes/markdown.svg", + ".java": "expui/fileTypes/java.svg", + ".xml": "expui/fileTypes/xml.svg", + ".xsd": "expui/fileTypes/xsd.svg", + ".wsdl": "expui/fileTypes/wsdl.svg", + ".xhtml": "expui/fileTypes/xhtml.svg", + ".yml": "expui/fileTypes/yaml.svg", + ".yaml": "expui/fileTypes/yaml.svg", + ".html": "expui/fileTypes/html.svg", + ".htm": "expui/fileTypes/html.svg", + ".css": "expui/fileTypes/css.svg", + ".scss": "expui/fileTypes/css.svg", + ".less": "expui/fileTypes/css.svg", + ".sass": "expui/fileTypes/css.svg", + ".sql": "expui/fileTypes/sql.svg", + ".sh": "expui/fileTypes/shell.svg", + ".bash": "expui/fileTypes/shell.svg", + ".zsh": "expui/fileTypes/shell.svg", + ".properties": "expui/fileTypes/properties.svg", + ".toml": "expui/fileTypes/toml.svg", + ".csv": "expui/fileTypes/csv.svg", + ".http": "expui/fileTypes/http.svg", + ".png": "expui/fileTypes/image.svg", + ".jpg": "expui/fileTypes/image.svg", + ".jpeg": "expui/fileTypes/image.svg", + ".gif": "expui/fileTypes/image.svg", + ".svg": "expui/fileTypes/image.svg", + ".webp": "expui/fileTypes/image.svg", + ".ico": "expui/fileTypes/image.svg", + ".bmp": "expui/fileTypes/image.svg", + ".zip": "expui/fileTypes/archive.svg", + ".tar": "expui/fileTypes/archive.svg", + ".gz": "expui/fileTypes/archive.svg", + ".tgz": "expui/fileTypes/archive.svg", + ".7z": "expui/fileTypes/archive.svg", + ".rar": "expui/fileTypes/archive.svg", + ".ttf": "expui/fileTypes/font.svg", + ".otf": "expui/fileTypes/font.svg", + ".woff": "expui/fileTypes/font.svg", + ".woff2": "expui/fileTypes/font.svg", + ".vue": "expui/fileTypes/vue.svg", + ".graphql": "expui/fileTypes/graphql.svg", + ".gql": "expui/fileTypes/graphql.svg", + ".tf": "expui/fileTypes/terraform.svg", + ".hcl": "expui/fileTypes/terraform.svg", + ".swift": "expui/fileTypes/swiftLang.svg", + ".c": "expui/fileTypes/c.svg", + ".h": "expui/fileTypes/h.svg", + ".cpp": "expui/fileTypes/cpp.svg", + ".cc": "expui/fileTypes/cpp.svg", + ".cxx": "expui/fileTypes/cpp.svg", + ".hpp": "expui/fileTypes/h.svg", + ".cs": "expui/fileTypes/Csharp.svg", + ".jsp": "expui/fileTypes/jsp.svg", + ".gradle": "expui/fileTypes/gradle.svg", + ".kts": "expui/fileTypes/gradle.svg", + ".groovy": "expui/fileTypes/groovy.svg", + ".pl": "expui/fileTypes/perl.svg", + ".rst": "expui/fileTypes/rst.svg", + ".patch": "expui/fileTypes/patch.svg", + ".diff": "expui/fileTypes/patch.svg", + ".txt": "expui/fileTypes/text.svg", + ".log": "expui/fileTypes/text.svg", + ".lock": "expui/fileTypes/text.svg" + }, + "filenames": { + "\u0000lithe:java.class": "expui/nodes/class.svg", + "\u0000lithe:java.interface": "expui/nodes/interface.svg", + "\u0000lithe:java.enum": "expui/nodes/enum.svg", + "\u0000lithe:java.annotation": "expui/nodes/annotation.svg", + "\u0000lithe:java.record": "expui/nodes/record.svg", + "\u0000lithe:java.exception": "expui/nodes/exception.svg", + "pom.xml": { + "source": "../../../plugins/maven/src/main/resources/images/expui/build/mavenProject.svg", + "darkSource": "../../../plugins/maven/src/main/resources/images/expui/build/mavenProject_dark.svg", + "asset": "expui/fileTypes/maven.svg", + "darkAsset": "expui/fileTypes/maven_dark.svg", + "definition": "idea-maven" + }, + "dockerfile": "expui/fileTypes/docker.svg", + ".gitignore": "expui/fileTypes/gitignore.svg", + ".editorconfig": "expui/fileTypes/editorConfig.svg", + ".env": "expui/fileTypes/properties.svg", + "license": "expui/fileTypes/text.svg", + "notice": "expui/fileTypes/text.svg" + }, + "folders": { + "\u0000lithe:folder.source-root": "expui/nodes/sourceRoot.svg", + "\u0000lithe:folder.test-root": "expui/nodes/testRoot.svg", + "\u0000lithe:folder.resources-root": "expui/nodes/resourcesRoot.svg", + "\u0000lithe:folder.test-resources-root": "expui/nodes/testResourcesRoot.svg", + "\u0000lithe:folder.package": "expui/nodes/package.svg" + }, + "defaultFile": "expui/fileTypes/text.svg", + "defaultFolder": "expui/nodes/folder.svg" +} diff --git a/windows/tauri/scripts/idea-icon-mappings.json b/windows/tauri/scripts/idea-icon-mappings.json new file mode 100644 index 000000000..33263c57d --- /dev/null +++ b/windows/tauri/scripts/idea-icon-mappings.json @@ -0,0 +1,880 @@ +{ + "description": "Lithe icon export name -> IntelliJ platform icon asset (relative to intellij-community platform/icons/src). Managed by scripts/generate-idea-icons.ts. Icons absent from this list keep their lucide fallback. colorPolicy defaults to 'fixed' (IDEA colors as-is); 'fallback' entries document why an IDEA asset was not used.", + "icons": [ + { + "icon": "AiLoadingIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArchiveIcon", + "source": "expui/fileTypes/archive.svg" + }, + { + "icon": "ArrowBendDownLeftIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowClockwiseIcon", + "source": "expui/general/redo.svg" + }, + { + "icon": "ArrowCounterClockwiseIcon", + "source": "expui/general/undo.svg" + }, + { + "icon": "ArrowDownIcon", + "source": "expui/general/down.svg" + }, + { + "icon": "ArrowFatLineDownIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowLeftIcon", + "source": "expui/general/left.svg" + }, + { + "icon": "ArrowRightIcon", + "source": "expui/general/right.svg" + }, + { + "icon": "ArrowSquareOutIcon", + "source": "expui/ide/externalLink.svg" + }, + { + "icon": "ArrowUpIcon", + "source": "expui/general/up.svg" + }, + { + "icon": "ArrowsClockwiseIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowsInIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowsInLineVerticalIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowsLeftRightIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ArrowsOutIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "BellIcon", + "source": "expui/toolwindows/notifications.svg" + }, + { + "icon": "BookOpenIcon", + "source": "expui/toolwindows/documentation.svg" + }, + { + "icon": "BookmarkIcon", + "source": "expui/bookmarks/bookmark.svg" + }, + { + "icon": "BracketsCurlyIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "BrainIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "BroomIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "BugBeetleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "BugIcon", + "source": "expui/run/debug.svg" + }, + { + "icon": "CalendarIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CaretDoubleLeftIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CaretDoubleRightIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CaretDoubleUpIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CaretDownIcon", + "source": "expui/general/chevronDown.svg" + }, + { + "icon": "CaretLeftIcon", + "source": "expui/general/chevronLeft.svg" + }, + { + "icon": "CaretRightIcon", + "source": "expui/general/chevronRight.svg" + }, + { + "icon": "CaretUpIcon", + "source": "expui/general/chevronUp.svg" + }, + { + "icon": "ChatCircleTextIcon", + "source": "expui/toolwindows/messages.svg" + }, + { + "icon": "CheckCircleIcon", + "source": "expui/general/successDialog.svg" + }, + { + "icon": "CheckIcon", + "source": "expui/general/greenCheckmark.svg" + }, + { + "icon": "ChevronDownIcon", + "source": "expui/general/chevronDown.svg" + }, + { + "icon": "ChevronLeftIcon", + "source": "expui/general/chevronLeft.svg" + }, + { + "icon": "ChevronRightIcon", + "source": "expui/general/chevronRight.svg" + }, + { + "icon": "ChevronUpIcon", + "source": "expui/general/chevronUp.svg" + }, + { + "icon": "CircleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CirclesThreeIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ClipboardIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ClipboardTextIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ClockCounterClockwiseIcon", + "source": "expui/general/history.svg" + }, + { + "icon": "ClockIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CloudArrowDownIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CloudCheckIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CloudIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CloudSlashIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CloudWarningIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CodeBlockIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CodeIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ColumnsIcon", + "source": "expui/general/splitVertically.svg" + }, + { + "icon": "CommandIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CopyIcon", + "source": "expui/general/copy.svg" + }, + { + "icon": "CopySimpleIcon", + "source": "expui/general/copy.svg" + }, + { + "icon": "CornersInIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CornersOutIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CreditCardIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CubeIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "CursorClickIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "DatabaseIcon", + "source": "expui/toolwindows/toolWindowDataView.svg" + }, + { + "icon": "DotOutlineIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "DotsThreeIcon", + "source": "expui/general/moreHorizontal.svg" + }, + { + "icon": "DownloadIcon", + "source": "expui/general/download.svg" + }, + { + "icon": "DownloadSimpleIcon", + "source": "expui/general/download.svg" + }, + { + "icon": "EyeIcon", + "source": "expui/general/show.svg" + }, + { + "icon": "EyeSlashIcon", + "source": "expui/general/hide.svg" + }, + { + "icon": "FadersHorizontalIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FileCodeIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FileIcon", + "source": "fileTypes/text.svg" + }, + { + "icon": "FilePlusIcon", + "source": "expui/actions/addFile.svg" + }, + { + "icon": "FileTextIcon", + "source": "fileTypes/text.svg" + }, + { + "icon": "FilesIcon", + "source": "expui/general/listFiles.svg" + }, + { + "icon": "FlipHorizontalIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FlipVerticalIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FloppyDiskIcon", + "source": "expui/general/save.svg" + }, + { + "icon": "FolderIcon", + "source": "expui/nodes/folder.svg" + }, + { + "icon": "FolderOpenIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FolderPlusIcon", + "source": "expui/actions/newFolder.svg" + }, + { + "icon": "FolderSimpleStarIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "FunctionIcon", + "source": "expui/nodes/function.svg" + }, + { + "icon": "FunnelIcon", + "source": "expui/general/filter.svg" + }, + { + "icon": "GearIcon", + "source": "expui/general/settings.svg" + }, + { + "icon": "GearSixIcon", + "source": "expui/general/settings.svg" + }, + { + "icon": "GitBranchIcon", + "source": "vcs/branch.svg" + }, + { + "icon": "GitCommitIcon", + "source": "expui/vcs/commit.svg" + }, + { + "icon": "GitDiffIcon", + "source": "expui/vcs/diff.svg" + }, + { + "icon": "GitGraphIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "GitMergeIcon", + "source": "expui/vcs/merge.svg" + }, + { + "icon": "GitPullRequestIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "GithubLogoIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "GlobeHemisphereWestIcon", + "source": "expui/toolwindows/web.svg" + }, + { + "icon": "GlobeIcon", + "source": "expui/toolwindows/web.svg" + }, + { + "icon": "HardDrivesIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "HashIcon", + "source": "expui/general/hashtag.svg" + }, + { + "icon": "HouseIcon", + "source": "expui/javaee/home.svg" + }, + { + "icon": "ImageIcon", + "source": "expui/fileTypes/image.svg" + }, + { + "icon": "InfoIcon", + "source": "expui/general/informationDialog.svg" + }, + { + "icon": "KeyIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "KeyboardIcon", + "source": "expui/general/keyboard.svg" + }, + { + "icon": "LaptopIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "LightbulbIcon", + "source": "expui/actions/realIntentionBulb.svg" + }, + { + "icon": "LightningIcon", + "source": "expui/actions/lightning.svg" + }, + { + "icon": "LightningSlashIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "LinkIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "LinkSimpleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ListBulletsIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ListChecksIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ListIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "LockIcon", + "source": "expui/general/locked.svg" + }, + { + "icon": "LockKeyIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "LockOpenIcon", + "source": "expui/general/unlocked.svg" + }, + { + "icon": "MagicWandIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "MagnifyingGlassIcon", + "source": "expui/general/search.svg" + }, + { + "icon": "MagnifyingGlassMinusIcon", + "source": "expui/image/zoomOut.svg" + }, + { + "icon": "MagnifyingGlassPlusIcon", + "source": "expui/image/zoomIn.svg" + }, + { + "icon": "MegaphoneIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "MicrophoneIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "MinusCircleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "MinusIcon", + "source": "expui/general/remove.svg" + }, + { + "icon": "MoneyIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "MonitorIcon", + "source": "expui/nodes/desktop.svg" + }, + { + "icon": "MoonIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "NavigationArrowIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "NetworkIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "NodesIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "OpenExternalIcon", + "source": "expui/ide/externalLink.svg" + }, + { + "icon": "PackageIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PaintBrushIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PaletteIcon", + "source": "expui/toolwindows/palette.svg" + }, + { + "icon": "PaperPlaneTiltIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PauseIcon", + "source": "expui/run/pause.svg" + }, + { + "icon": "PenIcon", + "source": "expui/general/edit.svg" + }, + { + "icon": "PencilIcon", + "source": "expui/general/edit.svg" + }, + { + "icon": "PencilLineIcon", + "source": "expui/general/edit.svg" + }, + { + "icon": "PencilSimpleIcon", + "source": "expui/general/edit.svg" + }, + { + "icon": "PencilSimpleLineIcon", + "source": "expui/general/edit.svg" + }, + { + "icon": "PlayCircleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PlayIcon", + "source": "expui/run/run.svg" + }, + { + "icon": "PlugsConnectedIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PlusCircleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PlusIcon", + "source": "expui/general/add.svg" + }, + { + "icon": "PulseIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PushPinIcon", + "source": "expui/general/pin.svg" + }, + { + "icon": "PushPinSlashIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "PuzzlePieceIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "QuestionIcon", + "source": "expui/general/questionDialog.svg" + }, + { + "icon": "RadioButtonIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "RefreshIcon", + "source": "expui/general/refresh.svg" + }, + { + "icon": "RobotIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "RocketLaunchIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "RowsIcon", + "source": "expui/general/splitHorizontally.svg" + }, + { + "icon": "RowsPlusTopIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ScissorsIcon", + "source": "expui/general/cut.svg" + }, + { + "icon": "ShieldCheckIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ShieldIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "ShieldWarningIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SidebarSimpleIcon", + "source": "expui/general/layout.svg" + }, + { + "icon": "SignInIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SignOutIcon", + "source": "expui/general/exit.svg" + }, + { + "icon": "SlidersHorizontalIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SlidersIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SparkleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SquareIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "SquaresFourIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "StackIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "StopIcon", + "source": "expui/run/stop.svg" + }, + { + "icon": "SunIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TableIcon", + "source": "expui/nodes/dataTables.svg" + }, + { + "icon": "TagIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TerminalIcon", + "source": null, + "note": "IDEA terminal tool window icon ships in the terminal plugin, not platform/icons; keep lucide fallback" + }, + { + "icon": "TerminalWindowIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextAaIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextAlignCenterIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextAlignJustifyIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextAlignLeftIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextIndentIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextOutdentIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TextTIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "TranslateIcon", + "source": "expui/general/language.svg" + }, + { + "icon": "TrashIcon", + "source": "expui/general/delete.svg" + }, + { + "icon": "TreeStructureIcon", + "source": "expui/general/tree.svg" + }, + { + "icon": "UploadIcon", + "source": "expui/general/upload.svg" + }, + { + "icon": "UploadSimpleIcon", + "source": "expui/general/upload.svg" + }, + { + "icon": "UserCircleIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "UserIcon", + "source": "expui/general/user.svg" + }, + { + "icon": "UsersThreeIcon", + "source": "expui/general/groups.svg" + }, + { + "icon": "WarningCircleIcon", + "source": "expui/general/warningDialog.svg" + }, + { + "icon": "WarningIcon", + "source": "expui/general/warningDialog.svg" + }, + { + "icon": "WindowExpandIcon", + "source": null, + "note": "no confident IntelliJ platform asset; lucide fallback" + }, + { + "icon": "WrenchIcon", + "source": "expui/general/externalTools.svg" + }, + { + "icon": "XCircleIcon", + "source": "expui/general/errorDialog.svg" + }, + { + "icon": "XIcon", + "source": "expui/general/close.svg" + } + ] +} diff --git a/windows/tauri/src-tauri/src/host.rs b/windows/tauri/src-tauri/src/host.rs index 3910d075f..350556cae 100644 --- a/windows/tauri/src-tauri/src/host.rs +++ b/windows/tauri/src-tauri/src/host.rs @@ -1,7 +1,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::fs; -use std::path::PathBuf; +use std::fs::{self, File}; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Mutex; use tauri::{AppHandle, Emitter, Manager, Theme, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; @@ -34,6 +35,13 @@ pub struct SymlinkInfo { is_dir: bool, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BoundedFileRead { + bytes: Vec, + truncated: bool, +} + pub struct PendingCliOpenRequests(Mutex>); #[derive(Debug, Clone, Deserialize, Serialize)] @@ -299,12 +307,29 @@ pub async fn create_app_window(app: AppHandle, request: Option) -> Result #[cfg(test)] mod tests { use super::{ - cli_payloads, copy_path, create_app_window, unique_destination, WINDOW_TASKBAR_ICON, + cli_payloads, copy_path, create_app_window, read_bounded, read_local_file_bounded, + unique_destination, WINDOW_TASKBAR_ICON, }; use std::fs; use std::future::Future; + use std::io::Read; use std::path::PathBuf; + struct CountingReader { + bytes_read: usize, + bytes_remaining: usize, + } + + impl Read for CountingReader { + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { + let count = buffer.len().min(self.bytes_remaining); + buffer[..count].fill(b'x'); + self.bytes_read += count; + self.bytes_remaining -= count; + Ok(count) + } + } + fn assert_async_window_command(_command: F) where F: Fn(tauri::AppHandle, Option) -> Fut, @@ -353,6 +378,41 @@ mod tests { fs::remove_dir_all(root).unwrap(); } + #[test] + fn bounded_reader_consumes_only_one_sentinel_byte_beyond_the_payload_limit() { + let limit = 16; + let mut reader = CountingReader { + bytes_read: 0, + bytes_remaining: limit + 32, + }; + + let result = read_bounded(&mut reader, limit).unwrap(); + + assert!(result.truncated); + assert_eq!(reader.bytes_read, limit + 1); + assert_eq!(result.bytes.len(), limit); + } + + #[test] + fn bounded_local_file_omits_oversized_content_from_the_ipc_payload() { + let limit = 16; + let path = std::env::temp_dir().join(format!( + "lithe-host-bounded-read-{}-{}", + std::process::id(), + super::WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); + fs::write(&path, vec![b'x'; limit + 32]).unwrap(); + + let result = read_local_file_bounded(path.clone(), limit).unwrap(); + let payload = serde_json::to_value(&result).unwrap(); + + assert!(result.truncated); + assert!(result.bytes.is_empty()); + assert!(payload["bytes"].as_array().unwrap().len() <= limit); + + fs::remove_file(path).unwrap(); + } + #[test] fn bundled_windows_icon_includes_taskbar_sizes() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("icons/icon.ico"); @@ -545,6 +605,37 @@ pub fn read_local_file(path: PathBuf) -> Result, String> { fs::read(path).map_err(|error| error.to_string()) } +fn read_bounded(reader: &mut impl Read, max_bytes: usize) -> io::Result { + let sentinel_limit = max_bytes + .checked_add(1) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Byte limit is too large"))?; + let read_limit = u64::try_from(sentinel_limit) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Byte limit is too large"))?; + let mut bytes = Vec::new(); + reader.take(read_limit).read_to_end(&mut bytes)?; + let truncated = bytes.len() > max_bytes; + bytes.truncate(max_bytes); + Ok(BoundedFileRead { bytes, truncated }) +} + +fn read_local_file_bounded_from_path(path: &Path, max_bytes: usize) -> io::Result { + let mut file = File::open(path)?; + let max_bytes_u64 = u64::try_from(max_bytes).unwrap_or(u64::MAX); + // Metadata avoids reading known oversized files; the sentinel byte below covers later growth. + if file.metadata()?.len() > max_bytes_u64 { + return Ok(BoundedFileRead { + bytes: Vec::new(), + truncated: true, + }); + } + read_bounded(&mut file, max_bytes) +} + +#[tauri::command] +pub fn read_local_file_bounded(path: PathBuf, max_bytes: usize) -> Result { + read_local_file_bounded_from_path(&path, max_bytes).map_err(|error| error.to_string()) +} + #[tauri::command] pub fn read_file_custom(path: PathBuf) -> Result { fs::read_to_string(path).map_err(|error| error.to_string()) diff --git a/windows/tauri/src-tauri/src/main.rs b/windows/tauri/src-tauri/src/main.rs index 5e4bf02c1..a8a31df1c 100644 --- a/windows/tauri/src-tauri/src/main.rs +++ b/windows/tauri/src-tauri/src/main.rs @@ -103,6 +103,7 @@ fn main() { host::validate_font, host::get_bundled_extensions_path, host::read_local_file, + host::read_local_file_bounded, host::read_file_custom, host::write_file, host::move_file, diff --git a/windows/tauri/src/extensions/bundled-extension-manifests.test.ts b/windows/tauri/src/extensions/bundled-extension-manifests.test.ts new file mode 100644 index 000000000..716df833a --- /dev/null +++ b/windows/tauri/src/extensions/bundled-extension-manifests.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import { bundledExtensionManifests } from "./bundled/bundled-extension-manifests"; + +function getIdeaIconTheme() { + const manifest = bundledExtensionManifests.find( + ({ manifest }) => manifest.id === "lithe.icon-theme.idea-icons", + )?.manifest; + const iconTheme = manifest?.icons?.find(({ id }) => id === "idea-icons"); + + if (!iconTheme) { + throw new Error("Bundled IDEA icon theme is missing"); + } + + return iconTheme; +} + +const IDEA_TREE_ICON_IDS = [ + "idea-maven", + "idea-class", + "idea-interface", + "idea-enum", + "idea-annotation", + "idea-record", + "idea-exception", + "idea-sourceRoot", + "idea-testRoot", + "idea-resourcesRoot", + "idea-testResourcesRoot", + "idea-package", +]; + +describe("bundled file icon themes", () => { + test("ships IDEA Icons without the retired Lithe icon theme", () => { + const extensionIds = bundledExtensionManifests.map(({ manifest }) => manifest.id); + + expect(extensionIds).toContain("lithe.icon-theme.idea-icons"); + expect(extensionIds).not.toContain("lithe.icon-theme.lithe-icons"); + }); + + test("maps the common IDEA project tree entries to ExpUI icons", () => { + const iconTheme = getIdeaIconTheme(); + + expect(iconTheme.fileExtensions?.[".java"]).toBe("idea-java"); + expect(iconTheme.filenames?.["pom.xml"]).toBe("idea-maven"); + expect(iconTheme.filenames?.[".gitignore"]).toBe("idea-gitignore"); + expect(iconTheme.filenames?.["\0lithe:java.class"]).toBe("idea-class"); + expect(iconTheme.filenames?.["\0lithe:java.exception"]).toBe("idea-exception"); + expect(iconTheme.folders?.["\0lithe:folder.source-root"]).toBe("idea-sourceRoot"); + expect(iconTheme.folders?.["\0lithe:folder.package"]).toBe("idea-package"); + expect(iconTheme.expandedFolders).toEqual(iconTheme.folders); + }); + + test("declares dark and light assets for every added IDEA tree icon", () => { + const iconTheme = getIdeaIconTheme(); + + for (const iconId of IDEA_TREE_ICON_IDS) { + expect(iconTheme.iconDefinitions[iconId]).toMatch(/_dark\.svg$/); + expect(iconTheme.lightIconDefinitions?.[iconId]).toMatch(/(? { + const iconTheme = getIdeaIconTheme(); + + for (const iconId of IDEA_TREE_ICON_IDS) { + const assetPaths = [ + iconTheme.iconDefinitions[iconId], + iconTheme.lightIconDefinitions?.[iconId], + ]; + + for (const assetPath of assetPaths) { + expect(assetPath).toBeDefined(); + const contents = await Bun.file( + new URL(`./bundled/icon-themes/idea/${assetPath!.replace(/^\.\//, "")}`, import.meta.url), + ).text(); + expect(contents.startsWith(" + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/archive_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/archive_dark.svg new file mode 100644 index 000000000..ab3cae2cc --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/archive_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c.svg new file mode 100644 index 000000000..b7a663f5a --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c_dark.svg new file mode 100644 index 000000000..af5455250 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/c_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp.svg new file mode 100644 index 000000000..e8c8c1949 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp_dark.svg new file mode 100644 index 000000000..3c4442388 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/cpp_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css.svg new file mode 100644 index 000000000..929203773 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css_dark.svg new file mode 100644 index 000000000..f541ee8c7 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/css_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv.svg new file mode 100644 index 000000000..cc5fa2eaa --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv_dark.svg new file mode 100644 index 000000000..d038177a9 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/csv_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker.svg new file mode 100644 index 000000000..a4f9cab91 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker_dark.svg new file mode 100644 index 000000000..5369a264a --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/docker_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig.svg new file mode 100644 index 000000000..547bba721 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig_dark.svg new file mode 100644 index 000000000..528d02ebe --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/editorConfig_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font.svg new file mode 100644 index 000000000..9e187b36f --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font_dark.svg new file mode 100644 index 000000000..94fbd32d9 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/font_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gitignore.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gitignore.svg new file mode 100644 index 000000000..86228c617 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gitignore.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle.svg new file mode 100644 index 000000000..29f3e59ee --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle_dark.svg new file mode 100644 index 000000000..37c055abf --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/gradle_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql.svg new file mode 100644 index 000000000..d4dd36ac3 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql_dark.svg new file mode 100644 index 000000000..a0095957d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/graphql_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy.svg new file mode 100644 index 000000000..671aff55e --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy_dark.svg new file mode 100644 index 000000000..0dfe6a462 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/groovy_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h.svg new file mode 100644 index 000000000..1905b901a --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h_dark.svg new file mode 100644 index 000000000..935071870 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/h_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html.svg new file mode 100644 index 000000000..c446cf39e --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html_dark.svg new file mode 100644 index 000000000..9e10d57f1 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/html_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/http.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/http.svg new file mode 100644 index 000000000..906237407 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/http.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image.svg new file mode 100644 index 000000000..9a09720fa --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image_dark.svg new file mode 100644 index 000000000..4f189a0e3 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/image_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java.svg new file mode 100644 index 000000000..00e1a4c96 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript.svg new file mode 100644 index 000000000..d7eb6cc8d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript_dark.svg new file mode 100644 index 000000000..188617076 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/javaScript_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java_dark.svg new file mode 100644 index 000000000..155295f59 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/java_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json.svg new file mode 100644 index 000000000..d8f707dd5 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json_dark.svg new file mode 100644 index 000000000..29371ab71 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/json_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp.svg new file mode 100644 index 000000000..564a2190c --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp_dark.svg new file mode 100644 index 000000000..1c04aa9cf --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/jsp_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown.svg new file mode 100644 index 000000000..c603fd236 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown_dark.svg new file mode 100644 index 000000000..2d86cd368 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/markdown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven.svg new file mode 100644 index 000000000..e22b73598 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven_dark.svg new file mode 100644 index 000000000..8f8f35e5c --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/maven_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch.svg new file mode 100644 index 000000000..b3a1d040b --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch_dark.svg new file mode 100644 index 000000000..e6d13133e --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/patch_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl.svg new file mode 100644 index 000000000..565044076 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl_dark.svg new file mode 100644 index 000000000..870ac8faa --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/perl_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties.svg new file mode 100644 index 000000000..547bba721 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties_dark.svg new file mode 100644 index 000000000..528d02ebe --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/properties_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst.svg new file mode 100644 index 000000000..09c5b340e --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst_dark.svg new file mode 100644 index 000000000..ae54b4de1 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/rst_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell.svg new file mode 100644 index 000000000..0676afdd7 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell_dark.svg new file mode 100644 index 000000000..7022d0c76 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/shell_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql.svg new file mode 100644 index 000000000..a57c04270 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql_dark.svg new file mode 100644 index 000000000..57d62ee80 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/sql_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang.svg new file mode 100644 index 000000000..4b6e68b65 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang.svg @@ -0,0 +1,3 @@ + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang_dark.svg new file mode 100644 index 000000000..5d386f489 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/swiftLang_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform.svg new file mode 100644 index 000000000..ac3ca7839 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform_dark.svg new file mode 100644 index 000000000..4c4d9bfee --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/terraform_dark.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text.svg new file mode 100644 index 000000000..110139cf6 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text_dark.svg new file mode 100644 index 000000000..86468276b --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/text_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml.svg new file mode 100644 index 000000000..10789f0e9 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml_dark.svg new file mode 100644 index 000000000..b9e6aea69 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/toml_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript.svg new file mode 100644 index 000000000..b4d487f0d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript_dark.svg new file mode 100644 index 000000000..5c74ff7f7 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/typeScript_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/vue.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/vue.svg new file mode 100644 index 000000000..8625428aa --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/vue.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl.svg new file mode 100644 index 000000000..a92e9c5c0 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl_dark.svg new file mode 100644 index 000000000..3732545ba --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/wsdl_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml.svg new file mode 100644 index 000000000..94b886f9b --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml_dark.svg new file mode 100644 index 000000000..432f62e24 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xhtml_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml.svg new file mode 100644 index 000000000..154c1eded --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml_dark.svg new file mode 100644 index 000000000..8a2eb779d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xml_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd.svg new file mode 100644 index 000000000..0c87edc93 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd_dark.svg new file mode 100644 index 000000000..c012bd1e6 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/xsd_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml.svg new file mode 100644 index 000000000..9d4031ee2 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml_dark.svg new file mode 100644 index 000000000..b173b95d9 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/fileTypes/yaml_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation.svg new file mode 100644 index 000000000..842956af7 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation_dark.svg new file mode 100644 index 000000000..66abc9160 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/annotation_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class.svg new file mode 100644 index 000000000..77996e71b --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class_dark.svg new file mode 100644 index 000000000..a8027940d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/class_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum.svg new file mode 100644 index 000000000..13deae631 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum_dark.svg new file mode 100644 index 000000000..44d6e5cf8 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/enum_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception.svg new file mode 100644 index 000000000..ab5bae754 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception_dark.svg new file mode 100644 index 000000000..710a292ec --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/exception_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder.svg new file mode 100644 index 000000000..7ece6b711 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder_dark.svg new file mode 100644 index 000000000..0af492bad --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/folder_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface.svg new file mode 100644 index 000000000..b7436907d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface_dark.svg new file mode 100644 index 000000000..53905c525 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/interface_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package.svg new file mode 100644 index 000000000..4fd24f914 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package_dark.svg new file mode 100644 index 000000000..f86ed61ba --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/package_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record.svg new file mode 100644 index 000000000..bafa1940d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record_dark.svg new file mode 100644 index 000000000..c965cf713 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/record_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot.svg new file mode 100644 index 000000000..a58f3256d --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot_dark.svg new file mode 100644 index 000000000..0a042e94e --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/resourcesRoot_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot.svg new file mode 100644 index 000000000..4a4b44608 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot_dark.svg new file mode 100644 index 000000000..7d3159c25 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/sourceRoot_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot.svg new file mode 100644 index 000000000..1098ebb34 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot_dark.svg new file mode 100644 index 000000000..221b77061 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testResourcesRoot_dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot.svg new file mode 100644 index 000000000..4bfdae1a5 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot_dark.svg b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot_dark.svg new file mode 100644 index 000000000..21261bb49 --- /dev/null +++ b/windows/tauri/src/extensions/bundled/icon-themes/idea/icons/expui/nodes/testRoot_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/windows/tauri/src/extensions/icon-themes/bundled-icon-theme-assets.ts b/windows/tauri/src/extensions/icon-themes/bundled-icon-theme-assets.ts index 735a07b6f..b8f83c4df 100644 --- a/windows/tauri/src/extensions/icon-themes/bundled-icon-theme-assets.ts +++ b/windows/tauri/src/extensions/icon-themes/bundled-icon-theme-assets.ts @@ -1,5 +1,5 @@ const BUNDLED_ICON_THEME_ASSETS = import.meta.glob( - "../bundled/icon-themes/{lithe,material,pierre,symbols}/**/*.svg", + "../bundled/icon-themes/{idea,material,pierre,symbols}/**/*.svg", { eager: true, import: "default", @@ -8,7 +8,7 @@ const BUNDLED_ICON_THEME_ASSETS = import.meta.glob( ) as Record; const BUNDLED_ICON_THEME_DIRECTORIES: Record = { - "lithe.icon-theme.lithe-icons": "lithe", + "lithe.icon-theme.idea-icons": "idea", "lithe.icon-theme.material": "material", "lithe.icon-theme.pierre": "pierre", "lithe.icon-theme.symbols": "symbols", diff --git a/windows/tauri/src/extensions/icon-themes/components/themed-file-icon.tsx b/windows/tauri/src/extensions/icon-themes/components/themed-file-icon.tsx index f5898e1f2..1a077d91b 100644 --- a/windows/tauri/src/extensions/icon-themes/components/themed-file-icon.tsx +++ b/windows/tauri/src/extensions/icon-themes/components/themed-file-icon.tsx @@ -5,6 +5,7 @@ import { getDefaultSetting, useSettingsStore } from "@/features/settings/stores/ import { useTranslation } from "@/i18n/locale-provider"; import { cn } from "@/utils/cn"; import { iconThemeRegistry } from "../icon-theme-registry"; +import { getSemanticFileIconLookupName, type FileIconSemanticKind } from "../file-icon-semantics"; const THEMED_FILE_ICON_CACHE_KEY = import.meta.env.DEV ? Date.now().toString(36) : ""; @@ -18,6 +19,7 @@ interface ThemedFileIconProps { isDir: boolean; isExpanded?: boolean; isSymlink?: boolean; + semanticKind?: FileIconSemanticKind | null; className?: string; } @@ -26,6 +28,7 @@ export function ThemedFileIcon({ isDir, isExpanded = false, isSymlink = false, + semanticKind, className = "text-subtle-foreground", }: ThemedFileIconProps) { const { t } = useTranslation(); @@ -43,10 +46,11 @@ export function ThemedFileIcon({ const iconTheme = iconThemeRegistry.getTheme(iconThemeId) ?? iconThemeRegistry.getTheme(getDefaultSetting("iconTheme")); + const lookupFileName = getSemanticFileIconLookupName(iconThemeId, fileName, semanticKind); const iconResult = useMemo( - () => iconTheme?.getFileIcon(fileName, isDir, isExpanded, isSymlink) ?? null, - [fileName, iconTheme, isDir, isExpanded, isSymlink, colorThemeId], + () => iconTheme?.getFileIcon(lookupFileName, isDir, isExpanded, isSymlink) ?? null, + [lookupFileName, iconTheme, isDir, isExpanded, isSymlink, colorThemeId], ); const sanitizedSvg = useMemo( () => diff --git a/windows/tauri/src/extensions/icon-themes/file-icon-semantics.test.ts b/windows/tauri/src/extensions/icon-themes/file-icon-semantics.test.ts new file mode 100644 index 000000000..4b2dd034f --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/file-icon-semantics.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test"; +import { getSemanticFileIconLookupName } from "./file-icon-semantics"; + +test("IDEA Icons use reserved semantic lookups without affecting other themes", () => { + expect(getSemanticFileIconLookupName("idea-icons", "Example.java", "java.class")).toBe( + "\0lithe:java.class", + ); + expect(getSemanticFileIconLookupName("idea-icons", "java", "folder.source-root")).toBe( + "\0lithe:folder.source-root", + ); + expect(getSemanticFileIconLookupName("material-icons", "Example.java", "java.class")).toBe( + "Example.java", + ); + expect(getSemanticFileIconLookupName("idea-icons", "Example.java", null)).toBe("Example.java"); +}); diff --git a/windows/tauri/src/extensions/icon-themes/file-icon-semantics.ts b/windows/tauri/src/extensions/icon-themes/file-icon-semantics.ts new file mode 100644 index 000000000..f9ff556a8 --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/file-icon-semantics.ts @@ -0,0 +1,37 @@ +export type FileIconSemanticKind = + | "java.class" + | "java.interface" + | "java.enum" + | "java.annotation" + | "java.record" + | "java.exception" + | "folder.source-root" + | "folder.test-root" + | "folder.resources-root" + | "folder.test-resources-root" + | "folder.package"; + +export const IDEA_ICON_THEME_ID = "idea-icons"; + +const IDEA_SEMANTIC_LOOKUP_NAMES: Record = { + "java.class": "\0lithe:java.class", + "java.interface": "\0lithe:java.interface", + "java.enum": "\0lithe:java.enum", + "java.annotation": "\0lithe:java.annotation", + "java.record": "\0lithe:java.record", + "java.exception": "\0lithe:java.exception", + "folder.source-root": "\0lithe:folder.source-root", + "folder.test-root": "\0lithe:folder.test-root", + "folder.resources-root": "\0lithe:folder.resources-root", + "folder.test-resources-root": "\0lithe:folder.test-resources-root", + "folder.package": "\0lithe:folder.package", +}; + +export function getSemanticFileIconLookupName( + iconThemeId: string, + fileName: string, + semanticKind?: FileIconSemanticKind | null, +): string { + if (iconThemeId !== IDEA_ICON_THEME_ID || !semanticKind) return fileName; + return IDEA_SEMANTIC_LOOKUP_NAMES[semanticKind]; +} diff --git a/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.test.ts b/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.test.ts new file mode 100644 index 000000000..b2d013b4c --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "bun:test"; + +test("Java icon cache keeps external-change invalidation for every icon surface", async () => { + const source = await Bun.file(new URL("./use-java-file-icon-kind.ts", import.meta.url)).text(); + const tabBarSource = await Bun.file( + new URL("../../../features/tabs/components/tab-bar.tsx", import.meta.url), + ).text(); + const rowSubscription = source.slice( + source.indexOf("function subscribeToPath"), + source.indexOf("export function useJavaFileIconCacheLifecycle"), + ); + const treeLifecycle = source.slice( + source.indexOf("export function useJavaFileIconCacheLifecycle"), + ); + + expect(rowSubscription).not.toContain('addEventListener("file-external-change"'); + expect(treeLifecycle).toContain('addEventListener("file-external-change"'); + expect(treeLifecycle).toContain('removeEventListener("file-external-change"'); + expect(treeLifecycle).toContain("workspaceRuntimeRegistry.subscribe(handleWorkspaceChange)"); + expect(tabBarSource).toContain("useJavaFileIconCacheLifecycle();"); +}); diff --git a/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.ts b/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.ts new file mode 100644 index 000000000..859f7a73b --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/hooks/use-java-file-icon-kind.ts @@ -0,0 +1,288 @@ +import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react"; +import { IDEA_ICON_THEME_ID } from "@/extensions/icon-themes/file-icon-semantics"; +import { + detectJavaFileIconSemanticKind, + type JavaFileIconSemanticKind, +} from "@/extensions/icon-themes/java-file-kind"; +import { isUtf8WithinByteLimit } from "@/extensions/icon-themes/utf8-byte-limit"; +import { useBufferStore } from "@/features/editor/stores/buffer.store"; +import { getSourceEditorBufferByPath } from "@/features/editor/utils/buffer-index"; +import { readFileWithinByteLimit } from "@/features/file-system/controllers/bounded-file-read"; +import { useSettingsStore } from "@/features/settings/stores/settings.store"; +import { workspaceRuntimeRegistry } from "@/features/workspace/runtime/workspace-runtime-registry"; +import { frontendTrace } from "@/utils/frontend-trace"; + +interface FileExternalChangeDetail { + path?: string; +} + +interface PendingJavaFileRead { + fileName: string; + generation: number; + path: string; + taskKey: string; + epoch: number; +} + +const MAX_CONCURRENT_JAVA_ICON_READS = 4; +const MAX_JAVA_ICON_SOURCE_BYTES = 256 * 1024; +const MAX_JAVA_ICON_CACHE_ENTRIES = 512; +const JAVA_ICON_READ_TIMEOUT_MS = 5_000; +const JAVA_ICON_RETRY_DELAY_MS = 1_000; +const javaFileKindCache = new Map(); +const javaFileReadEpochs = new Map(); +const javaFileReadFailureCounts = new Map(); +const javaFileRetryAfter = new Map(); +const queuedJavaFileTasks = new Set(); +const pendingJavaFileReads: PendingJavaFileRead[] = []; +const pathSubscribers = new Map void>>(); +let activeJavaFileReads = 0; +let workspaceGeneration = 0; +let firstReadFailureLogged = false; +let cacheLifecycleRefCount = 0; +let lifecycleWorkspaceUnsubscribe: (() => void) | null = null; + +function notifyPath(path: string) { + for (const callback of pathSubscribers.get(path) ?? []) callback(); +} + +function notifyAllPaths() { + for (const path of pathSubscribers.keys()) notifyPath(path); +} + +function rememberJavaFileKind(path: string, kind: JavaFileIconSemanticKind | null) { + javaFileKindCache.delete(path); + javaFileKindCache.set(path, kind); + + while (javaFileKindCache.size > MAX_JAVA_ICON_CACHE_ENTRIES) { + const oldestPath = javaFileKindCache.keys().next().value; + if (typeof oldestPath !== "string") break; + javaFileKindCache.delete(oldestPath); + } +} + +function invalidateJavaFilePath(path: string) { + javaFileKindCache.delete(path); + javaFileReadFailureCounts.delete(path); + javaFileRetryAfter.delete(path); + javaFileReadEpochs.set(path, (javaFileReadEpochs.get(path) ?? 0) + 1); + notifyPath(path); +} + +function handleExternalFileChange(event: Event) { + const path = (event as CustomEvent).detail?.path; + if (!path || !path.toLowerCase().endsWith(".java")) return; + invalidateJavaFilePath(path); +} + +function handleWorkspaceChange() { + workspaceGeneration += 1; + javaFileKindCache.clear(); + javaFileReadEpochs.clear(); + javaFileReadFailureCounts.clear(); + javaFileRetryAfter.clear(); + pendingJavaFileReads.length = 0; + queuedJavaFileTasks.clear(); + notifyAllPaths(); +} + +function subscribeToPath(path: string, callback: () => void) { + const subscribers = pathSubscribers.get(path) ?? new Set<() => void>(); + subscribers.add(callback); + pathSubscribers.set(path, subscribers); + + return () => { + const currentSubscribers = pathSubscribers.get(path); + currentSubscribers?.delete(callback); + if (currentSubscribers?.size === 0) pathSubscribers.delete(path); + }; +} + +export function useJavaFileIconCacheLifecycle() { + useEffect(() => { + cacheLifecycleRefCount += 1; + if (cacheLifecycleRefCount === 1) { + window.addEventListener("file-external-change", handleExternalFileChange); + lifecycleWorkspaceUnsubscribe = workspaceRuntimeRegistry.subscribe(handleWorkspaceChange); + } + + return () => { + cacheLifecycleRefCount = Math.max(0, cacheLifecycleRefCount - 1); + if (cacheLifecycleRefCount === 0) { + window.removeEventListener("file-external-change", handleExternalFileChange); + lifecycleWorkspaceUnsubscribe?.(); + lifecycleWorkspaceUnsubscribe = null; + handleWorkspaceChange(); + } + }; + }, []); +} + +function isEligibleLocalJavaIconPath(path: string): boolean { + if (!/^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\")) return false; + const segments = path.replace(/\\/g, "/").toLowerCase().split("/"); + return !segments.some((segment) => + ["target", "build", "out", "generated", "generated-sources"].includes(segment), + ); +} + +async function readJavaSourceWithinLimit(path: string): Promise { + return readFileWithinByteLimit(path, MAX_JAVA_ICON_SOURCE_BYTES); +} + +function withTimeout(promise: Promise, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timeoutId = window.setTimeout( + () => reject(new Error("Java icon source read timed out")), + timeoutMs, + ); + promise.then( + (value) => { + window.clearTimeout(timeoutId); + resolve(value); + }, + (error) => { + window.clearTimeout(timeoutId); + reject(error); + }, + ); + }); +} + +function scheduleRetry(path: string, fileName: string, generation: number) { + const retryAt = Date.now() + JAVA_ICON_RETRY_DELAY_MS; + javaFileRetryAfter.set(path, retryAt); + window.setTimeout(() => { + if (generation !== workspaceGeneration || javaFileRetryAfter.get(path) !== retryAt) return; + javaFileRetryAfter.delete(path); + queueJavaFileRead(path, fileName); + }, JAVA_ICON_RETRY_DELAY_MS); +} + +function recordReadFailure(pendingRead: PendingJavaFileRead, error: unknown) { + if (pendingRead.generation !== workspaceGeneration) return; + + const failureCount = (javaFileReadFailureCounts.get(pendingRead.path) ?? 0) + 1; + javaFileReadFailureCounts.set(pendingRead.path, failureCount); + if (failureCount >= 2) { + rememberJavaFileKind(pendingRead.path, null); + } else { + scheduleRetry(pendingRead.path, pendingRead.fileName, pendingRead.generation); + } + + if (!firstReadFailureLogged) { + firstReadFailureLogged = true; + frontendTrace("warn", "file-icons", "javaIconSemanticRead:error", { + path: pendingRead.path, + error: error instanceof Error ? error.message : String(error), + }); + } +} + +function runPendingJavaFileReads() { + while (activeJavaFileReads < MAX_CONCURRENT_JAVA_ICON_READS && pendingJavaFileReads.length > 0) { + const pendingRead = pendingJavaFileReads.shift(); + if (!pendingRead) return; + + if (pendingRead.generation !== workspaceGeneration) { + queuedJavaFileTasks.delete(pendingRead.taskKey); + continue; + } + + activeJavaFileReads += 1; + void withTimeout(readJavaSourceWithinLimit(pendingRead.path), JAVA_ICON_READ_TIMEOUT_MS) + .then((source) => { + if (pendingRead.generation !== workspaceGeneration) return; + if ((javaFileReadEpochs.get(pendingRead.path) ?? 0) !== pendingRead.epoch) return; + const kind = + source === null ? null : detectJavaFileIconSemanticKind(pendingRead.fileName, source); + rememberJavaFileKind(pendingRead.path, kind); + javaFileReadFailureCounts.delete(pendingRead.path); + javaFileRetryAfter.delete(pendingRead.path); + }) + .catch((error) => recordReadFailure(pendingRead, error)) + .finally(() => { + activeJavaFileReads -= 1; + queuedJavaFileTasks.delete(pendingRead.taskKey); + notifyPath(pendingRead.path); + + const currentEpoch = javaFileReadEpochs.get(pendingRead.path) ?? 0; + if ( + pendingRead.generation === workspaceGeneration && + currentEpoch !== pendingRead.epoch && + !javaFileKindCache.has(pendingRead.path) + ) { + queueJavaFileRead(pendingRead.path, pendingRead.fileName); + } + runPendingJavaFileReads(); + }); + } +} + +function queueJavaFileRead(path: string, fileName: string) { + const epoch = javaFileReadEpochs.get(path) ?? 0; + const taskKey = `${workspaceGeneration}\0${path}`; + if ( + !isEligibleLocalJavaIconPath(path) || + javaFileKindCache.has(path) || + queuedJavaFileTasks.has(taskKey) || + (javaFileRetryAfter.get(path) ?? 0) > Date.now() + ) { + return; + } + + queuedJavaFileTasks.add(taskKey); + pendingJavaFileReads.push({ + path, + fileName, + epoch, + generation: workspaceGeneration, + taskKey, + }); + runPendingJavaFileReads(); +} + +export function useJavaFileIconKind( + path: string, + fileName: string, + isDirectory: boolean, + enabled: boolean, +): JavaFileIconSemanticKind | null { + const iconThemeId = useSettingsStore((state) => state.settings.iconTheme); + const isJavaFile = !isDirectory && fileName.toLowerCase().endsWith(".java"); + const semanticIconsEnabled = + enabled && + iconThemeId === IDEA_ICON_THEME_ID && + isJavaFile && + isEligibleLocalJavaIconPath(path); + const sourceBufferContent = useBufferStore((state) => { + if (!semanticIconsEnabled) return null; + return getSourceEditorBufferByPath(state.buffers, path)?.content ?? null; + }); + const subscribe = useCallback( + (callback: () => void) => + semanticIconsEnabled ? subscribeToPath(path, callback) : () => undefined, + [path, semanticIconsEnabled], + ); + const diskKind = useSyncExternalStore( + subscribe, + () => (semanticIconsEnabled ? javaFileKindCache.get(path) : undefined), + () => undefined, + ); + const bufferKind = useMemo( + () => + sourceBufferContent === null || + !isUtf8WithinByteLimit(sourceBufferContent, MAX_JAVA_ICON_SOURCE_BYTES) + ? null + : detectJavaFileIconSemanticKind(fileName, sourceBufferContent), + [fileName, sourceBufferContent], + ); + + useEffect(() => { + if (!semanticIconsEnabled || sourceBufferContent !== null) return; + queueJavaFileRead(path, fileName); + }, [diskKind, fileName, path, semanticIconsEnabled, sourceBufferContent]); + + if (!semanticIconsEnabled) return null; + return sourceBufferContent === null ? (diskKind ?? null) : bufferKind; +} diff --git a/windows/tauri/src/extensions/icon-themes/java-file-kind.test.ts b/windows/tauri/src/extensions/icon-themes/java-file-kind.test.ts new file mode 100644 index 000000000..9ad04739d --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/java-file-kind.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import { detectJavaFileIconSemanticKind, type JavaFileIconSemanticKind } from "./java-file-kind"; + +describe("Java file icon semantics", () => { + const cases: Array<{ + fileName: string; + source: string; + expected: JavaFileIconSemanticKind; + }> = [ + { fileName: "Example.java", source: "public class Example {}", expected: "java.class" }, + { + fileName: "Example.java", + source: "public interface Example {}", + expected: "java.interface", + }, + { + fileName: "Example.java", + source: "public enum Example { VALUE }", + expected: "java.enum", + }, + { + fileName: "Example.java", + source: "public @interface Example {}", + expected: "java.annotation", + }, + { + fileName: "Example.java", + source: "public record Example(String value) {}", + expected: "java.record", + }, + { + fileName: "Example.java", + source: "public class Example extends java.lang.RuntimeException {}", + expected: "java.exception", + }, + ]; + + test.each(cases)("detects $fileName as $expected", ({ fileName, source, expected }) => { + expect(detectJavaFileIconSemanticKind(fileName, source)).toBe(expected); + }); + + test("ignores declarations inside comments, strings, text blocks, and nested types", () => { + const source = ` + // class Example {} + class Container { + String text = "interface Example {}"; + String block = """enum Example { VALUE }"""; + class Example {} + } + `; + + expect(detectJavaFileIconSemanticKind("Example.java", source)).toBeNull(); + }); + + test("uses the matching top-level declaration instead of the first declaration", () => { + const source = "class Helper {}\npublic interface Example {}"; + expect(detectJavaFileIconSemanticKind("Example.java", source)).toBe("java.interface"); + }); + + test("does not infer exception semantics from a filename or custom base class", () => { + expect( + detectJavaFileIconSemanticKind( + "ExampleException.java", + "class ExampleException extends DomainFailure {}", + ), + ).toBe("java.class"); + expect(detectJavaFileIconSemanticKind("Other.java", "class Example {}")).toBeNull(); + }); + + test("does not confuse a generic bound with the direct superclass", () => { + expect( + detectJavaFileIconSemanticKind("Example.java", "class Example {}"), + ).toBe("java.class"); + expect( + detectJavaFileIconSemanticKind( + "Example.java", + "class Example> extends RuntimeException {}", + ), + ).toBe("java.exception"); + }); +}); diff --git a/windows/tauri/src/extensions/icon-themes/java-file-kind.ts b/windows/tauri/src/extensions/icon-themes/java-file-kind.ts new file mode 100644 index 000000000..010aa2540 --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/java-file-kind.ts @@ -0,0 +1,185 @@ +import type { FileIconSemanticKind } from "./file-icon-semantics"; + +export type JavaFileIconSemanticKind = Extract< + FileIconSemanticKind, + | "java.class" + | "java.interface" + | "java.enum" + | "java.annotation" + | "java.record" + | "java.exception" +>; + +function maskJavaCommentsAndLiterals(source: string): string { + let output = ""; + let index = 0; + let state: "code" | "line-comment" | "block-comment" | "string" | "char" | "text-block" = "code"; + + const appendMasked = (value: string) => { + output += value.replace(/[^\r\n]/g, " "); + }; + + while (index < source.length) { + const nextTwo = source.slice(index, index + 2); + const nextThree = source.slice(index, index + 3); + const character = source[index] ?? ""; + + if (state === "code") { + if (nextTwo === "//") { + appendMasked(nextTwo); + index += 2; + state = "line-comment"; + continue; + } + if (nextTwo === "/*") { + appendMasked(nextTwo); + index += 2; + state = "block-comment"; + continue; + } + if (nextThree === '"""') { + appendMasked(nextThree); + index += 3; + state = "text-block"; + continue; + } + if (character === '"') { + appendMasked(character); + index += 1; + state = "string"; + continue; + } + if (character === "'") { + appendMasked(character); + index += 1; + state = "char"; + continue; + } + + output += character; + index += 1; + continue; + } + + if (state === "line-comment") { + appendMasked(character); + index += 1; + if (character === "\n") state = "code"; + continue; + } + + if (state === "block-comment") { + if (nextTwo === "*/") { + appendMasked(nextTwo); + index += 2; + state = "code"; + } else { + appendMasked(character); + index += 1; + } + continue; + } + + if (state === "text-block") { + if (nextThree === '"""') { + appendMasked(nextThree); + index += 3; + state = "code"; + } else { + appendMasked(character); + index += 1; + } + continue; + } + + if (character === "\\") { + appendMasked(source.slice(index, index + 2)); + index += Math.min(2, source.length - index); + continue; + } + + appendMasked(character); + index += 1; + if ((state === "string" && character === '"') || (state === "char" && character === "'")) { + state = "code"; + } + } + + return output; +} + +function getJavaBaseName(fileName: string): string | null { + const leafName = fileName.split(/[\\/]/).pop() ?? fileName; + if (!leafName.toLowerCase().endsWith(".java")) return null; + return leafName.slice(0, -".java".length); +} + +function stripLeadingTypeParameters(declarationTail: string): string { + let index = 0; + while (/\s/.test(declarationTail[index] ?? "")) index += 1; + if (declarationTail[index] !== "<") return declarationTail.slice(index); + + let depth = 0; + for (; index < declarationTail.length; index += 1) { + const character = declarationTail[index]; + if (character === "<") depth += 1; + if (character === ">") { + depth -= 1; + if (depth === 0) return declarationTail.slice(index + 1); + } + } + return ""; +} + +export function detectJavaFileIconSemanticKind( + fileName: string, + source: string, +): JavaFileIconSemanticKind | null { + const baseName = getJavaBaseName(fileName); + if (!baseName) return null; + + const maskedSource = maskJavaCommentsAndLiterals(source); + const declarationPattern = + /@interface\s+([A-Za-z_$][\w$]*)|\b(class|interface|enum|record)\s+([A-Za-z_$][\w$]*)/g; + let braceDepth = 0; + let scanIndex = 0; + + for (const match of maskedSource.matchAll(declarationPattern)) { + const matchIndex = match.index ?? 0; + while (scanIndex < matchIndex) { + const character = maskedSource[scanIndex]; + if (character === "{") braceDepth += 1; + if (character === "}") braceDepth = Math.max(0, braceDepth - 1); + scanIndex += 1; + } + + if (braceDepth !== 0) continue; + + const annotationName = match[1]; + const declarationKind = match[2]; + const declarationName = annotationName ?? match[3]; + if (declarationName !== baseName) continue; + + if (annotationName) return "java.annotation"; + if (declarationKind === "interface") return "java.interface"; + if (declarationKind === "enum") return "java.enum"; + if (declarationKind === "record") return "java.record"; + + const declarationEnd = maskedSource.indexOf("{", matchIndex); + const declarationTail = maskedSource.slice( + matchIndex + match[0].length, + declarationEnd === -1 ? maskedSource.length : declarationEnd, + ); + if ( + /\bextends\s+(?:java\.lang\.)?(?:RuntimeException|Exception|Throwable|Error)\b/.test( + stripLeadingTypeParameters(declarationTail), + ) + ) { + return "java.exception"; + } + + return "java.class"; + } + + return null; +} diff --git a/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.test.ts b/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.test.ts new file mode 100644 index 000000000..1001a9467 --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test"; +import { isUtf8WithinByteLimit } from "./utf8-byte-limit"; + +describe("isUtf8WithinByteLimit", () => { + test("measures ASCII source by UTF-8 bytes", () => { + expect(isUtf8WithinByteLimit("class", 5)).toBe(true); + expect(isUtf8WithinByteLimit("class", 4)).toBe(false); + }); + + test("counts multibyte characters and surrogate pairs by encoded size", () => { + expect(isUtf8WithinByteLimit("é中😀", 9)).toBe(true); + expect(isUtf8WithinByteLimit("é中😀", 8)).toBe(false); + }); + + test("matches TextEncoder replacement semantics for unpaired surrogates", () => { + expect(isUtf8WithinByteLimit("\ud800", 3)).toBe(true); + expect(isUtf8WithinByteLimit("\ud800", 2)).toBe(false); + }); +}); diff --git a/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.ts b/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.ts new file mode 100644 index 000000000..e748abb22 --- /dev/null +++ b/windows/tauri/src/extensions/icon-themes/utf8-byte-limit.ts @@ -0,0 +1,32 @@ +export function isUtf8WithinByteLimit(value: string, maxBytes: number): boolean { + if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) { + throw new RangeError("UTF-8 byte limit must be a non-negative safe integer"); + } + + let byteLength = 0; + for (let index = 0; index < value.length; index += 1) { + const codeUnit = value.charCodeAt(index); + let encodedLength: number; + if (codeUnit <= 0x7f) { + encodedLength = 1; + } else if (codeUnit <= 0x7ff) { + encodedLength = 2; + } else if ( + codeUnit >= 0xd800 && + codeUnit <= 0xdbff && + index + 1 < value.length && + value.charCodeAt(index + 1) >= 0xdc00 && + value.charCodeAt(index + 1) <= 0xdfff + ) { + encodedLength = 4; + index += 1; + } else { + // TextEncoder replaces unpaired surrogates with the three-byte replacement character. + encodedLength = 3; + } + + if (byteLength > maxBytes - encodedLength) return false; + byteLength += encodedLength; + } + return true; +} diff --git a/windows/tauri/src/extensions/themes/builtin/lithe.json b/windows/tauri/src/extensions/themes/builtin/lithe.json index 5ef661fee..f340924ac 100644 --- a/windows/tauri/src/extensions/themes/builtin/lithe.json +++ b/windows/tauri/src/extensions/themes/builtin/lithe.json @@ -14,18 +14,18 @@ "foreground": "#1f2328", "muted-foreground": "#4f5965", "subtle-foreground": "#68717d", - "border": "#dde1e6", - "accent": "#f0f2f5", - "selected": "#e8ebef", - "selection": "rgba(8, 119, 193, 0.2)", - "primary": "#0877c1", + "border": "#dfe1e5", + "accent": "#edf3ff", + "selected": "#d4e2ff", + "selection": "rgba(53, 116, 240, 0.2)", + "primary": "#3574f0", "cursor": "#1f2328", - "cursor-vim-normal": "rgba(8, 119, 193, 0.62)", - "cursor-vim-insert": "#0877c1", + "cursor-vim-normal": "rgba(53, 116, 240, 0.62)", + "cursor-vim-insert": "#3574f0", "destructive": "#cf3f4f", "success": "#27864f", "warning": "#a86400", - "info": "#0877c1", + "info": "#3574f0", "git-modified": "#a86400", "git-modified-staged": "#bd7411", "git-added": "#27864f", @@ -76,23 +76,23 @@ "description": "Layered neutral surfaces with readable text and Lithe blue accents", "appearance": "dark", "colors": { - "background": "#151619", - "surface": "#0f1012", - "foreground": "#f2f3f5", - "muted-foreground": "#c4c9d1", + "background": "#1e1f22", + "surface": "#2b2d30", + "foreground": "#dfe1e5", + "muted-foreground": "#b4b8bf", "subtle-foreground": "#8b929e", - "border": "#2b2f36", - "accent": "#202328", - "selected": "#282c33", - "selection": "rgba(40, 149, 211, 0.3)", - "primary": "#2895d3", - "cursor": "#f2f3f5", - "cursor-vim-normal": "rgba(40, 149, 211, 0.68)", - "cursor-vim-insert": "#2895d3", - "destructive": "#f16d75", - "success": "#4cc38a", - "warning": "#d9a441", - "info": "#58a6e7", + "border": "#43454a", + "accent": "#393b40", + "selected": "#2e436e", + "selection": "#214283", + "primary": "#3574f0", + "cursor": "#ced0d6", + "cursor-vim-normal": "rgba(53, 116, 240, 0.68)", + "cursor-vim-insert": "#3574f0", + "destructive": "#db5c5c", + "success": "#57965c", + "warning": "#d6ae58", + "info": "#548af7", "git-modified": "#d9a441", "git-modified-staged": "#e5b75e", "git-added": "#4cc38a", @@ -117,24 +117,24 @@ "terminal-bright-white": "#ffffff" }, "syntax": { - "comment": "#7f8793", - "keyword": "#e879c6", - "string": "#8ccf9f", - "number": "#e2a96b", - "function": "#61c0bf", - "variable": "#c8a2f4", - "tag": "#61c0bf", - "attribute": "#e2a96b", - "punctuation": "#a2a9b4", - "constant": "#e2a96b", - "property": "#6cb6f1", - "type": "#8fb8ff", - "operator": "#e879c6", - "boolean": "#e879c6", - "null": "#c8a2f4", - "regex": "#8ccf9f", - "jsx": "#61c0bf", - "jsx-attribute": "#e2a96b" + "comment": "#7a7e85", + "keyword": "#cf8e6d", + "string": "#6aab73", + "number": "#2aacb8", + "function": "#56a8f5", + "variable": "#bcbec4", + "tag": "#d5b778", + "attribute": "#b3ae60", + "punctuation": "#bcbec4", + "constant": "#c77dbb", + "property": "#c77dbb", + "type": "#bcbec4", + "operator": "#bcbec4", + "boolean": "#cf8e6d", + "null": "#cf8e6d", + "regex": "#6aab73", + "jsx": "#d5b778", + "jsx-attribute": "#b3ae60" } } ] diff --git a/windows/tauri/src/features/editor/components/toolbar/breadcrumb.tsx b/windows/tauri/src/features/editor/components/toolbar/breadcrumb.tsx index 8e44f2cb7..43c8d7fec 100644 --- a/windows/tauri/src/features/editor/components/toolbar/breadcrumb.tsx +++ b/windows/tauri/src/features/editor/components/toolbar/breadcrumb.tsx @@ -202,8 +202,8 @@ export default function Breadcrumb({ return ( <> -
-
+
+
{showPath && showBreadcrumbPath ? ( <> {showFilePath ? ( diff --git a/windows/tauri/src/features/editor/engines/monaco/theme.ts b/windows/tauri/src/features/editor/engines/monaco/theme.ts index e23bf3791..9ec2f238e 100644 --- a/windows/tauri/src/features/editor/engines/monaco/theme.ts +++ b/windows/tauri/src/features/editor/engines/monaco/theme.ts @@ -77,8 +77,8 @@ function createMonacoThemeData( "editorLink.activeForeground": accent, "editorCursor.foreground": cursor, "editor.selectionBackground": selection, - "editor.inactiveSelectionBackground": selected, - "editor.lineHighlightBackground": selected, + "editor.inactiveSelectionBackground": border, + "editor.lineHighlightBackground": secondaryBackground, "editorLineNumber.foreground": subtleForeground, "editorLineNumber.activeForeground": foreground, "editorIndentGuide.background1": border, diff --git a/windows/tauri/src/features/file-explorer/components/file-explorer-surface.test.ts b/windows/tauri/src/features/file-explorer/components/file-explorer-surface.test.ts new file mode 100644 index 000000000..26c4ff795 --- /dev/null +++ b/windows/tauri/src/features/file-explorer/components/file-explorer-surface.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "bun:test"; + +function sourceSection(source: string, start: string, end: string) { + return source.slice(source.indexOf(start), source.indexOf(end)); +} + +test("file explorer chrome uses the editor background token", async () => { + const sidebarSource = await Bun.file( + new URL("../../../ui/sidebar.tsx", import.meta.url), + ).text(); + const fileExplorerSource = await Bun.file( + new URL("./file-explorer-pane.tsx", import.meta.url), + ).text(); + const fileExplorerStyles = await Bun.file( + new URL("../styles/file-explorer-tree.css", import.meta.url), + ).text(); + + const panel = sourceSection( + sidebarSource, + "export function SidebarPanel", + "export function SidebarTitleBar", + ); + const toolbar = sourceSection( + sidebarSource, + "export function SidebarToolbar", + "export const SidebarFooter", + ); + const header = sourceSection( + sidebarSource, + "export function SidebarHeader", + "export function SidebarComposerBody", + ); + + for (const chrome of [panel, toolbar, header]) { + expect(chrome).toContain("bg-background"); + expect(chrome).not.toContain("bg-surface"); + } + const fileExplorerHeaderStyles = sourceSection( + fileExplorerStyles, + ".file-explorer-header {", + ".file-explorer-header-title", + ); + expect(fileExplorerHeaderStyles).toContain("background-color: var(--background)"); + expect(fileExplorerHeaderStyles).not.toContain("background-color: var(--surface)"); + expect(fileExplorerSource).toContain(''); +}); diff --git a/windows/tauri/src/features/file-explorer/components/file-explorer-tree-item.tsx b/windows/tauri/src/features/file-explorer/components/file-explorer-tree-item.tsx index 1405b0ba5..3ca81415f 100644 --- a/windows/tauri/src/features/file-explorer/components/file-explorer-tree-item.tsx +++ b/windows/tauri/src/features/file-explorer/components/file-explorer-tree-item.tsx @@ -1,6 +1,9 @@ import type React from "react"; import { memo } from "react"; +import type { FileIconSemanticKind } from "@/extensions/icon-themes/file-icon-semantics"; import type { FileTreeGitStatusDecoration } from "@/features/file-explorer/lib/file-tree-git-status"; +import { FILE_TREE_BASE_INDENT } from "@/features/file-explorer/lib/file-tree-row"; +import { useJavaFileIconKind } from "@/extensions/icon-themes/hooks/use-java-file-icon-kind"; import type { FileEntry } from "@/features/file-system/types/app.types"; import { InlineRenameInput } from "@/ui/input"; import { SidebarTreeDisclosure, SidebarTreeRow } from "@/features/sidebar/components/sidebar-tree"; @@ -8,8 +11,6 @@ import { cn } from "@/utils/cn"; import { ThemedFileIcon } from "@/extensions/icon-themes/components/themed-file-icon"; import { useTranslation } from "@/i18n/locale-provider"; -const FILE_TREE_BASE_INDENT = 10; - export interface FileTreeGuideTarget { path: string; name: string; @@ -49,6 +50,8 @@ interface FileExplorerTreeItemProps { showIndentGuides: boolean; isExpanded: boolean; isActive: boolean; + isRoot: boolean; + semanticKind?: FileIconSemanticKind; isCut: boolean; isDragOver: boolean; isDragging: boolean; @@ -94,6 +97,8 @@ function FileExplorerTreeItemComponent({ showIndentGuides, isExpanded, isActive, + isRoot, + semanticKind, isCut, isDragOver, isDragging, @@ -106,6 +111,8 @@ function FileExplorerTreeItemComponent({ rowId, }: FileExplorerTreeItemProps) { const { t } = useTranslation(); + const javaSemanticKind = useJavaFileIconKind(file.path, file.name, file.isDir, showIcon); + const resolvedSemanticKind = semanticKind ?? javaSemanticKind; const paddingLeft = FILE_TREE_BASE_INDENT + depth * indentSize; const gitStatusDecoration = getGitStatusDecoration(file); const guideLevels = Array.from({ length: depth }, (_, level) => level); @@ -143,7 +150,7 @@ function FileExplorerTreeItemComponent({
{renderTreeGuides()}
) : null} ) : null } label={ - + {renderHighlightedLabel(displayName ?? file.name, searchQuery)} } @@ -254,6 +268,8 @@ export const FileExplorerTreeItem = memo( prev.showIndentGuides === next.showIndentGuides && prev.isExpanded === next.isExpanded && prev.isActive === next.isActive && + prev.isRoot === next.isRoot && + prev.semanticKind === next.semanticKind && prev.isCut === next.isCut && prev.isDragOver === next.isDragOver && prev.isDragging === next.isDragging && diff --git a/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx b/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx index b622aae36..4a5c61ddc 100644 --- a/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx +++ b/windows/tauri/src/features/file-explorer/components/file-explorer-tree.tsx @@ -79,6 +79,7 @@ import { getVisibleFileTreeRowKey, useFileExplorerVisibleRows, } from "../hooks/use-file-explorer-visible-rows"; +import { useJavaFileIconCacheLifecycle } from "@/extensions/icon-themes/hooks/use-java-file-icon-kind"; import { FileExplorerViewport, type FileExplorerViewportHandle } from "./file-explorer-viewport"; import { FileExplorerTreeItem } from "./file-explorer-tree-item"; import type { FileTreeGuideTarget } from "./file-explorer-tree-item"; @@ -164,6 +165,7 @@ function FileExplorerTreeComponent({ onUploadFile, onFileMove, }: FileExplorerTreeProps) { + useJavaFileIconCacheLifecycle(); const [deleteCandidate, setDeleteCandidate] = useState<{ path: string; isDir: boolean; @@ -483,6 +485,7 @@ function FileExplorerTreeComponent({ }, [currentResolvedTreeSearch, isTreeSearchActive]); const { rowHeight, visibleRows, visibleRowIndexByPath } = useFileExplorerVisibleRows({ files: displayedFiles, + semanticFiles: files, expandedPathsOverride: displayedExpandedPaths, rootFolderPath, }); @@ -1101,10 +1104,11 @@ function FileExplorerTreeComponent({ return (
{ setHasTreeFocus(true); setFocusedPath((current) => current || activePath || visibleRows[0]?.file.path); @@ -1275,10 +1279,13 @@ function FileExplorerTreeComponent({ onMouseLeave={handleContainerMouseLeave} > event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} > +

+ {t("workbench.project")} +

{ diff --git a/windows/tauri/src/features/file-explorer/hooks/use-file-explorer-visible-rows.ts b/windows/tauri/src/features/file-explorer/hooks/use-file-explorer-visible-rows.ts index 93b4ff33f..9cf13b3e4 100644 --- a/windows/tauri/src/features/file-explorer/hooks/use-file-explorer-visible-rows.ts +++ b/windows/tauri/src/features/file-explorer/hooks/use-file-explorer-visible-rows.ts @@ -1,6 +1,8 @@ import { useMemo } from "react"; import { useShallow } from "zustand/react/shallow"; +import { IDEA_ICON_THEME_ID } from "@/extensions/icon-themes/file-icon-semantics"; import { getFileTreeRowHeight } from "@/features/file-explorer/lib/file-tree-row"; +import { buildMavenDirectorySemantics } from "@/features/file-explorer/lib/maven-file-tree-semantics"; import { buildVisibleFileTreeRows, type VisibleFileTreeRow, @@ -11,6 +13,7 @@ import { useSettingsStore } from "@/features/settings/stores/settings.store"; interface UseFileExplorerVisibleRowsOptions { files: FileEntry[]; + semanticFiles?: FileEntry[]; expandedPathsOverride?: ReadonlySet; rootFolderPath?: string; } @@ -21,28 +24,42 @@ export function getVisibleFileTreeRowKey(rows: readonly VisibleFileTreeRow[], in export function useFileExplorerVisibleRows({ files, + semanticFiles, expandedPathsOverride, rootFolderPath, }: UseFileExplorerVisibleRowsOptions) { const expandedPaths = useFileTreeStore((state) => state.expandedPaths); - const { compactFolders, hideRootFolder, sortOrder, uiFontSize } = useSettingsStore( - useShallow((state) => ({ - compactFolders: state.settings.compactFoldersInFileTree, - hideRootFolder: state.settings.hideRootFolderInFileTree, - sortOrder: state.settings.fileTreeSortOrder, - uiFontSize: state.settings.uiFontSize, - })), - ); + const { compactFolders, hideRootFolder, iconTheme, showFileIcons, sortOrder, uiFontSize } = + useSettingsStore( + useShallow((state) => ({ + compactFolders: state.settings.compactFoldersInFileTree, + hideRootFolder: state.settings.hideRootFolderInFileTree, + iconTheme: state.settings.iconTheme, + showFileIcons: state.settings.showFileIconsInFileTree, + sortOrder: state.settings.fileTreeSortOrder, + uiFontSize: state.settings.uiFontSize, + })), + ); const rowHeight = getFileTreeRowHeight(uiFontSize); + const semanticPresentationEnabled = showFileIcons && iconTheme === IDEA_ICON_THEME_ID; + const directorySemantics = useMemo( + () => + semanticPresentationEnabled + ? buildMavenDirectorySemantics(semanticFiles ?? files) + : new Map(), + [files, semanticFiles, semanticPresentationEnabled], + ); const visibleRows = useMemo(() => { return buildVisibleFileTreeRows(files, expandedPathsOverride ?? expandedPaths, { compactFolders, + directorySemantics, hiddenRootPath: hideRootFolder ? rootFolderPath : undefined, sortOrder, }); }, [ compactFolders, + directorySemantics, expandedPaths, expandedPathsOverride, files, diff --git a/windows/tauri/src/features/file-explorer/lib/file-tree-row.test.ts b/windows/tauri/src/features/file-explorer/lib/file-tree-row.test.ts new file mode 100644 index 000000000..4109e92ba --- /dev/null +++ b/windows/tauri/src/features/file-explorer/lib/file-tree-row.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "bun:test"; +import { FILE_TREE_MIN_ROW_HEIGHT, getFileTreeRowHeight } from "./file-tree-row"; + +describe("file tree row geometry", () => { + test("keeps the default UI font on the compact IDEA-style baseline", () => { + expect(getFileTreeRowHeight(13)).toBe(FILE_TREE_MIN_ROW_HEIGHT); + }); + + test("allows larger accessibility font settings to grow the row", () => { + expect(getFileTreeRowHeight(15)).toBe(26.25); + expect(getFileTreeRowHeight(18)).toBe(30.3); + expect(getFileTreeRowHeight(18.5)).toBe(30.98); + }); +}); diff --git a/windows/tauri/src/features/file-explorer/lib/file-tree-row.ts b/windows/tauri/src/features/file-explorer/lib/file-tree-row.ts index eb658fff1..a21243d3b 100644 --- a/windows/tauri/src/features/file-explorer/lib/file-tree-row.ts +++ b/windows/tauri/src/features/file-explorer/lib/file-tree-row.ts @@ -1,12 +1,14 @@ +export const FILE_TREE_BASE_INDENT = 10; +export const FILE_TREE_MIN_ROW_HEIGHT = 24; + const FILE_TREE_ROW_LINE_HEIGHT = 1.35; -const FILE_TREE_ROW_VERTICAL_PADDING = 8; -const FILE_TREE_ROW_BORDER_WIDTH = 2; +const FILE_TREE_ROW_VERTICAL_CHROME = 6; export function getFileTreeRowHeight(uiFontSize: number): number { - const height = - uiFontSize * FILE_TREE_ROW_LINE_HEIGHT + - FILE_TREE_ROW_VERTICAL_PADDING + - FILE_TREE_ROW_BORDER_WIDTH; + const height = Math.max( + FILE_TREE_MIN_ROW_HEIGHT, + uiFontSize * FILE_TREE_ROW_LINE_HEIGHT + FILE_TREE_ROW_VERTICAL_CHROME, + ); return Number(height.toFixed(2)); } diff --git a/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.test.ts b/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.test.ts new file mode 100644 index 000000000..4ec1fecdf --- /dev/null +++ b/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from "bun:test"; +import type { FileEntry } from "@/features/file-system/types/app.types"; +import { buildMavenDirectorySemantics } from "./maven-file-tree-semantics"; + +function directory(name: string, path: string, children: FileEntry[] = []): FileEntry { + return { name, path, isDir: true, children }; +} + +function file(name: string, path: string): FileEntry { + return { name, path, isDir: false }; +} + +function createMavenTree(root = "D:\\project"): FileEntry[] { + return [ + directory("project", root, [ + directory("src", `${root}\\src`, [ + directory("main", `${root}\\src\\main`, [ + directory("java", `${root}\\src\\main\\java`, [ + directory("com", `${root}\\src\\main\\java\\com`, [ + directory("example", `${root}\\src\\main\\java\\com\\example`), + ]), + ]), + directory("resources", `${root}\\src\\main\\resources`), + ]), + directory("test", `${root}\\src\\test`, [ + directory("java", `${root}\\src\\test\\java`), + directory("resources", `${root}\\src\\test\\resources`), + ]), + ]), + file("pom.xml", `${root}\\pom.xml`), + ]), + ]; +} + +describe("Maven directory icon semantics", () => { + test("recognizes standard roots and Java packages in a loaded Maven module", () => { + const semantics = buildMavenDirectorySemantics(createMavenTree()); + + expect(semantics.get("D:\\project\\src\\main\\java")).toBe("folder.source-root"); + expect(semantics.get("D:\\project\\src\\main\\java\\com\\example")).toBe("folder.package"); + expect(semantics.get("D:\\project\\src\\test\\java")).toBe("folder.test-root"); + expect(semantics.get("D:\\project\\src\\main\\resources")).toBe("folder.resources-root"); + expect(semantics.get("D:\\project\\src\\test\\resources")).toBe("folder.test-resources-root"); + }); + + test("does not label standard-looking paths without a loaded pom.xml", () => { + const [root] = createMavenTree(); + root!.children = root!.children?.filter((entry) => entry.name !== "pom.xml"); + + expect(buildMavenDirectorySemantics([root!]).size).toBe(0); + }); + + test("does not label invalid Java package directory names", () => { + const tree = createMavenTree(); + const javaRoot = tree[0]?.children?.[0]?.children?.[0]?.children?.[0]; + javaRoot?.children?.push( + directory("not-a-package", "D:\\project\\src\\main\\java\\not-a-package"), + ); + + expect( + buildMavenDirectorySemantics(tree).get("D:\\project\\src\\main\\java\\not-a-package"), + ).toBeUndefined(); + }); + + test("rejects Java keywords while allowing Unicode identifiers", () => { + const tree = createMavenTree(); + const javaRoot = tree[0]?.children?.[0]?.children?.[0]?.children?.[0]; + javaRoot?.children?.push( + directory("class", "D:\\project\\src\\main\\java\\class"), + directory("示例", "D:\\project\\src\\main\\java\\示例"), + ); + const semantics = buildMavenDirectorySemantics(tree); + + expect(semantics.get("D:\\project\\src\\main\\java\\class")).toBeUndefined(); + expect(semantics.get("D:\\project\\src\\main\\java\\示例")).toBe("folder.package"); + }); +}); diff --git a/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.ts b/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.ts new file mode 100644 index 000000000..af8deddc2 --- /dev/null +++ b/windows/tauri/src/features/file-explorer/lib/maven-file-tree-semantics.ts @@ -0,0 +1,192 @@ +import type { FileIconSemanticKind } from "@/extensions/icon-themes/file-icon-semantics"; +import type { FileEntry } from "@/features/file-system/types/app.types"; +import { + getDirName, + getRelativePath, + joinPath, + normalizePath, + pathStartsWithRoot, + stripTrailingPathSeparators, +} from "@/utils/path-helpers"; + +type DirectorySemanticKind = Extract< + FileIconSemanticKind, + | "folder.source-root" + | "folder.test-root" + | "folder.resources-root" + | "folder.test-resources-root" + | "folder.package" +>; + +interface StandardMavenRoot { + path: string; + kind: Exclude; + containsPackages: boolean; +} + +const JAVA_RESERVED_PACKAGE_SEGMENTS = new Set([ + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "exports", + "extends", + "false", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "module", + "native", + "new", + "null", + "open", + "opens", + "package", + "permits", + "private", + "protected", + "provides", + "public", + "record", + "requires", + "return", + "sealed", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "to", + "transient", + "transitive", + "true", + "try", + "uses", + "var", + "void", + "volatile", + "while", + "with", + "yield", + "_", +]); + +function comparablePath(path: string): string { + const normalized = normalizePath(stripTrailingPathSeparators(path)); + return /^[A-Za-z]:\//.test(normalized) ? normalized.toLowerCase() : normalized; +} + +function pathsEqual(left: string, right: string): boolean { + return comparablePath(left) === comparablePath(right); +} + +function isJavaPackagePath(relativePath: string): boolean { + const segments = normalizePath(relativePath).split("/").filter(Boolean); + return ( + segments.length > 0 && + segments.every( + (segment) => + !JAVA_RESERVED_PACKAGE_SEGMENTS.has(segment) && + /^[\p{ID_Start}_$][\p{ID_Continue}$\u200C\u200D]*$/u.test(segment), + ) + ); +} + +function collectEntries(entries: readonly FileEntry[], output: FileEntry[]) { + for (const entry of entries) { + output.push(entry); + if (entry.children) collectEntries(entry.children, output); + } +} + +export function buildMavenDirectorySemantics( + files: readonly FileEntry[], +): ReadonlyMap { + const entries: FileEntry[] = []; + collectEntries(files, entries); + + const moduleRoots = Array.from( + new Set( + entries + .filter( + (entry) => + !entry.isDir && + entry.name.toLowerCase() === "pom.xml" && + !entry.path.startsWith("remote://"), + ) + .map((entry) => getDirName(entry.path)), + ), + ).sort((left, right) => comparablePath(right).length - comparablePath(left).length); + + const standardRoots: StandardMavenRoot[] = moduleRoots.flatMap((moduleRoot) => [ + { + path: joinPath(moduleRoot, "src", "main", "java"), + kind: "folder.source-root", + containsPackages: true, + }, + { + path: joinPath(moduleRoot, "src", "test", "java"), + kind: "folder.test-root", + containsPackages: true, + }, + { + path: joinPath(moduleRoot, "src", "main", "resources"), + kind: "folder.resources-root", + containsPackages: false, + }, + { + path: joinPath(moduleRoot, "src", "test", "resources"), + kind: "folder.test-resources-root", + containsPackages: false, + }, + ]); + + const semantics = new Map(); + for (const entry of entries) { + if (!entry.isDir) continue; + + for (const standardRoot of standardRoots) { + if (pathsEqual(entry.path, standardRoot.path)) { + semantics.set(entry.path, standardRoot.kind); + break; + } + + if (!standardRoot.containsPackages || !pathStartsWithRoot(entry.path, standardRoot.path)) { + continue; + } + + const relativePath = getRelativePath(entry.path, standardRoot.path); + if (isJavaPackagePath(relativePath)) { + semantics.set(entry.path, "folder.package"); + break; + } + } + } + + return semantics; +} diff --git a/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.test.ts b/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.test.ts new file mode 100644 index 000000000..da48023ca --- /dev/null +++ b/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from "bun:test"; +import type { FileEntry } from "@/features/file-system/types/app.types"; +import { buildVisibleFileTreeRows } from "./visible-file-tree-rows"; + +function directory(name: string, path: string, children: FileEntry[] = []): FileEntry { + return { name, path, isDir: true, children }; +} + +test("compact folders preserve Maven source roots and compact packages with dots", () => { + const root = "D:\\project"; + const javaRoot = `${root}\\src\\main\\java`; + const packageRoot = `${javaRoot}\\com`; + const packageLeaf = `${packageRoot}\\example`; + const files = [ + directory("project", root, [ + directory("src", `${root}\\src`, [ + directory("main", `${root}\\src\\main`, [ + directory("java", javaRoot, [ + directory("com", packageRoot, [directory("example", packageLeaf)]), + ]), + directory("resources", `${root}\\src\\main\\resources`), + ]), + directory("test", `${root}\\src\\test`), + ]), + { name: "pom.xml", path: `${root}\\pom.xml`, isDir: false }, + ]), + ]; + const expandedPaths = new Set([ + root, + `${root}\\src`, + `${root}\\src\\main`, + javaRoot, + packageRoot, + packageLeaf, + ]); + + const rows = buildVisibleFileTreeRows(files, expandedPaths, { compactFolders: true }); + const sourceRootRow = rows.find((row) => row.file.path === javaRoot); + const packageRow = rows.find((row) => row.file.path === packageLeaf); + + expect(sourceRootRow?.displayName).toBeUndefined(); + expect(sourceRootRow?.semanticKind).toBe("folder.source-root"); + expect(packageRow?.displayName).toBe("com.example"); + expect(packageRow?.semanticKind).toBe("folder.package"); +}); + +test("compact package rows stop before an invalid package directory", () => { + const root = "D:\\project"; + const javaRoot = `${root}\\src\\main\\java`; + const packageRoot = `${javaRoot}\\com`; + const invalidChild = `${packageRoot}\\not-a-package`; + const files = [ + directory("project", root, [ + directory("src", `${root}\\src`, [ + directory("main", `${root}\\src\\main`, [ + directory("java", javaRoot, [ + directory("com", packageRoot, [directory("not-a-package", invalidChild)]), + ]), + ]), + ]), + { name: "pom.xml", path: `${root}\\pom.xml`, isDir: false }, + ]), + ]; + const expandedPaths = new Set([ + root, + `${root}\\src`, + `${root}\\src\\main`, + javaRoot, + packageRoot, + ]); + + const rows = buildVisibleFileTreeRows(files, expandedPaths, { compactFolders: true }); + + expect(rows.find((row) => row.file.path === packageRoot)?.semanticKind).toBe("folder.package"); + expect(rows.find((row) => row.file.path === packageRoot)?.displayName).toBeUndefined(); + expect(rows.find((row) => row.file.path === invalidChild)?.depth).toBeGreaterThan( + rows.find((row) => row.file.path === packageRoot)?.depth ?? -1, + ); +}); + +test("empty directory semantics preserve the original compact-folder projection", () => { + const root = "D:\\project"; + const javaRoot = `${root}\\src\\main\\java`; + const packageLeaf = `${javaRoot}\\com\\example`; + const files = [ + directory("project", root, [ + directory("src", `${root}\\src`, [ + directory("main", `${root}\\src\\main`, [ + directory("java", javaRoot, [ + directory("com", `${javaRoot}\\com`, [directory("example", packageLeaf)]), + ]), + ]), + ]), + { name: "pom.xml", path: `${root}\\pom.xml`, isDir: false }, + ]), + ]; + const expandedPaths = new Set([ + root, + `${root}\\src`, + `${root}\\src\\main`, + javaRoot, + `${javaRoot}\\com`, + packageLeaf, + ]); + + const rows = buildVisibleFileTreeRows(files, expandedPaths, { + compactFolders: true, + directorySemantics: new Map(), + }); + + expect(rows.find((row) => row.file.path === packageLeaf)?.displayName).toBe( + "src/main/java/com/example", + ); + expect(rows.find((row) => row.file.path === packageLeaf)?.semanticKind).toBeUndefined(); +}); diff --git a/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.ts b/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.ts index 18ec60148..11c697741 100644 --- a/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.ts +++ b/windows/tauri/src/features/file-explorer/lib/visible-file-tree-rows.ts @@ -1,5 +1,7 @@ import type { FileEntry } from "@/features/file-system/types/app.types"; +import type { FileIconSemanticKind } from "@/extensions/icon-themes/file-icon-semantics"; import { getCompactFolderChild } from "@/features/file-system/controllers/file-tree-utils"; +import { buildMavenDirectorySemantics } from "@/features/file-explorer/lib/maven-file-tree-semantics"; import type { FileTreeSortOrder } from "@/features/settings/types/settings.types"; import { getBaseName, getRelativePath, joinPath, pathStartsWithRoot } from "@/utils/path-helpers"; @@ -8,11 +10,13 @@ export interface VisibleFileTreeRow { depth: number; isExpanded: boolean; displayName?: string; + semanticKind?: FileIconSemanticKind; guideAncestors?: Array; } export interface BuildVisibleFileTreeRowsOptions { compactFolders?: boolean; + directorySemantics?: ReadonlyMap; hiddenRootPath?: string; sortOrder?: FileTreeSortOrder; } @@ -136,6 +140,7 @@ export function buildVisibleFileTreeRows( options: BuildVisibleFileTreeRowsOptions = {}, ): VisibleFileTreeRow[] { const rows: VisibleFileTreeRow[] = []; + const directorySemantics = options.directorySemantics ?? buildMavenDirectorySemantics(files); const compactFolders = options.compactFolders === true; const hiddenRootPath = options.hiddenRootPath; const sortOrder = options.sortOrder ?? "folders-first"; @@ -162,8 +167,16 @@ export function buildVisibleFileTreeRows( if (compactFolders) { while (expandedPaths.has(rowFile.path)) { + const currentSemanticKind = directorySemantics.get(rowFile.path); + if (currentSemanticKind && currentSemanticKind !== "folder.package") break; + const child = getCompactFolderChild(rowFile); if (!child) break; + const childSemanticKind = directorySemantics.get(child.path); + if (currentSemanticKind === "folder.package" && childSemanticKind !== "folder.package") { + break; + } + if (childSemanticKind && childSemanticKind !== "folder.package") break; rowFile = child; displayNameParts.push(child.name); @@ -171,11 +184,16 @@ export function buildVisibleFileTreeRows( } const isExpanded = rowFile.isDir && expandedPaths.has(rowFile.path); + const semanticKind = directorySemantics.get(rowFile.path); const row: VisibleFileTreeRow = { file: rowFile, depth, isExpanded, - displayName: displayNameParts.length > 1 ? displayNameParts.join("/") : undefined, + displayName: + displayNameParts.length > 1 + ? displayNameParts.join(semanticKind === "folder.package" ? "." : "/") + : undefined, + semanticKind, guideAncestors, }; rows.push(row); diff --git a/windows/tauri/src/features/file-explorer/styles/file-explorer-tree-guides.test.ts b/windows/tauri/src/features/file-explorer/styles/file-explorer-tree-guides.test.ts new file mode 100644 index 000000000..1d4f60285 --- /dev/null +++ b/windows/tauri/src/features/file-explorer/styles/file-explorer-tree-guides.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; + +test("file tree indent guides reveal without changing their interaction geometry", async () => { + const styles = await Bun.file(new URL("./file-explorer-tree.css", import.meta.url)).text(); + + expect(styles).toContain( + ".file-tree-container:has(.file-tree-guide:hover) [data-sidebar-tree-row] .file-tree-guide", + ); + expect(styles).toContain( + ".file-tree-container:has(:focus-visible) [data-sidebar-tree-row] .file-tree-guide", + ); + expect(styles).toContain("transition: opacity var(--app-duration-fast) var(--app-ease-smooth)"); + expect(styles).toMatch( + /\.file-tree-container \[data-sidebar-tree-row\] \.file-tree-guide \{[^}]*width: 7px;[^}]*opacity: 0;[^}]*pointer-events: auto;/s, + ); +}); diff --git a/windows/tauri/src/features/file-explorer/styles/file-explorer-tree.css b/windows/tauri/src/features/file-explorer/styles/file-explorer-tree.css index faab5a6bf..a2632d02c 100644 --- a/windows/tauri/src/features/file-explorer/styles/file-explorer-tree.css +++ b/windows/tauri/src/features/file-explorer/styles/file-explorer-tree.css @@ -1,4 +1,12 @@ .file-tree-container { + --file-tree-disclosure-size: 16px; + --file-tree-guide-icon-offset: 8px; + --file-tree-hover-bg: var(--accent); + --file-tree-icon-size: 16px; + --file-tree-row-inline-inset: 6px; + --file-tree-row-radius: 4px; + --file-tree-selected-idle-bg: var(--border); + --tree-guide-color: color-mix(in srgb, var(--subtle-foreground) 20%, transparent); position: relative; overflow-x: hidden; overflow-y: auto; @@ -20,10 +28,12 @@ .file-tree-container .file-tree-virtual-row { position: absolute; + box-sizing: border-box; right: 0; left: 0; width: 100%; min-width: 0; + padding-inline: var(--file-tree-row-inline-inset); } .file-tree-container .file-tree-item { @@ -35,7 +45,7 @@ .file-tree-container .file-tree-item button { box-sizing: border-box !important; border: 1px solid transparent !important; - border-radius: var(--radius-lg) !important; + border-radius: var(--file-tree-row-radius) !important; max-width: 100% !important; width: 100% !important; min-width: 0 !important; @@ -43,32 +53,46 @@ overflow: clip; display: flex !important; justify-content: flex-start !important; + column-gap: var(--lithe-chrome-gap) !important; + padding-block: 2px !important; } .file-tree-container .file-tree-item button:hover { - background-color: var(--file-tree-hover-bg) !important; + background-color: transparent !important; } .file-tree-container .file-tree-item button.bg-selected, .file-tree-container .file-tree-item button.bg-selected:hover { - border-color: color-mix(in srgb, var(--border) 78%, transparent) !important; + border-color: transparent !important; + background-color: var(--file-tree-selected-idle-bg) !important; +} + +.file-explorer-shell[data-tree-focused="true"] .file-tree-item button.bg-selected, +.file-explorer-shell[data-tree-focused="true"] .file-tree-item button.bg-selected:hover { background-color: var(--selected) !important; } .file-tree-container .file-tree-item button:focus-visible { - border-color: color-mix(in srgb, var(--primary) 42%, var(--border)) !important; + border-color: transparent !important; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 52%, var(--border)); } .file-tree-container .file-tree-item { position: relative; - --file-tree-hover-bg: color-mix(in srgb, var(--accent) 68%, transparent); - --file-tree-guide-icon-offset: 7px; - --tree-guide-color: color-mix(in srgb, var(--subtle-foreground) 18%, transparent); +} + +.file-tree-container [data-sidebar-tree-row]::before { + border-radius: var(--file-tree-row-radius); +} + +.file-tree-container [data-sidebar-tree-row]:hover::before { + background-color: var(--file-tree-hover-bg); } .file-tree-container .file-tree-row { position: relative; z-index: 2; + font-size: var(--ui-text-sm); box-sizing: border-box; height: 100%; max-width: 100%; @@ -83,10 +107,49 @@ text-overflow: ellipsis; } +.file-tree-container .file-tree-row [data-sidebar-tree-disclosure] { + width: var(--file-tree-disclosure-size); + height: var(--file-tree-disclosure-size); + margin-right: 0; +} + +.file-tree-container .file-tree-row [data-sidebar-tree-disclosure] svg { + width: 12px; + height: 12px; +} + +.file-tree-container .file-tree-node-icon { + width: var(--file-tree-icon-size) !important; + height: var(--file-tree-icon-size) !important; + line-height: var(--file-tree-icon-size); + text-align: center; +} + +.file-tree-container .file-tree-row[data-root="true"] .file-tree-node-label { + font-weight: 600; +} + .file-tree-container .file-tree-row input { min-width: 0; } +.file-tree-container .file-tree-edit-row input { + font-size: var(--ui-text-sm); +} + +.file-explorer-header { + border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent); + background-color: var(--background); + backdrop-filter: none; +} + +.file-explorer-header-title { + color: var(--foreground); + font-size: var(--ui-text-chrome); + font-weight: 600; + line-height: var(--lithe-chrome-line-height); +} + .file-explorer-base-text, .file-explorer-base-text * { font-size: var(--ui-text-sm); @@ -106,12 +169,19 @@ inset: 0; } -.file-tree-container .file-tree-guide { +.file-tree-container [data-sidebar-tree-row] .file-tree-guide { position: absolute; width: 7px; - opacity: 0.9; + opacity: 0; pointer-events: auto; transform: translateX(-3px); + transition: opacity var(--app-duration-fast) var(--app-ease-smooth); +} + +.file-tree-container:has(.file-tree-guide:hover) [data-sidebar-tree-row] .file-tree-guide, +.file-tree-container:focus-visible [data-sidebar-tree-row] .file-tree-guide, +.file-tree-container:has(:focus-visible) [data-sidebar-tree-row] .file-tree-guide { + opacity: 0.9; } .file-tree-container .file-tree-guide::before { diff --git a/windows/tauri/src/features/file-system/controllers/bounded-file-read.test.ts b/windows/tauri/src/features/file-system/controllers/bounded-file-read.test.ts new file mode 100644 index 000000000..0e1b35ddc --- /dev/null +++ b/windows/tauri/src/features/file-system/controllers/bounded-file-read.test.ts @@ -0,0 +1,40 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; + +const invoke = mock(async (_command: string, _args?: unknown): Promise => null); + +mock.module("@/platform/tauri-core", () => ({ invoke })); + +const { readFileWithinByteLimit } = await import("./bounded-file-read"); + +beforeEach(() => invoke.mockReset()); + +describe("readFileWithinByteLimit", () => { + test("passes the byte limit to the native bounded-read command", async () => { + invoke.mockResolvedValue({ + bytes: Array.from(new TextEncoder().encode("class App {}")), + truncated: false, + }); + + await expect(readFileWithinByteLimit("C:/workspace/App.java", 256 * 1024)).resolves.toBe( + "class App {}", + ); + expect(invoke).toHaveBeenCalledWith("read_local_file_bounded", { + path: "C:/workspace/App.java", + maxBytes: 256 * 1024, + }); + }); + + test("does not decode truncated or oversized host payloads", async () => { + invoke.mockResolvedValue({ bytes: [0x63, 0x6c, 0x61, 0x73], truncated: true }); + await expect(readFileWithinByteLimit("C:/workspace/App.java", 4)).resolves.toBeNull(); + + invoke.mockResolvedValue({ bytes: [0x63, 0x6c, 0x61, 0x73, 0x73], truncated: false }); + await expect(readFileWithinByteLimit("C:/workspace/App.java", 4)).resolves.toBeNull(); + }); + + test("rejects invalid UTF-8 without exposing it to semantic parsing", async () => { + invoke.mockResolvedValue({ bytes: [0xff], truncated: false }); + + await expect(readFileWithinByteLimit("C:/workspace/App.java", 4)).resolves.toBeNull(); + }); +}); diff --git a/windows/tauri/src/features/file-system/controllers/bounded-file-read.ts b/windows/tauri/src/features/file-system/controllers/bounded-file-read.ts new file mode 100644 index 000000000..d3b50d713 --- /dev/null +++ b/windows/tauri/src/features/file-system/controllers/bounded-file-read.ts @@ -0,0 +1,33 @@ +import { invoke } from "@/platform/tauri-core"; + +interface BoundedLocalFileRead { + bytes: ArrayBuffer | number[]; + truncated: boolean; +} + +const strictUtf8Decoder = new TextDecoder("utf-8", { fatal: true }); + +export async function readFileWithinByteLimit( + path: string, + maxBytes: number, +): Promise { + if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) { + throw new RangeError("File read byte limit must be a non-negative safe integer"); + } + + const response = await invoke("read_local_file_bounded", { + path, + maxBytes, + }); + const bytes = + response.bytes instanceof ArrayBuffer + ? new Uint8Array(response.bytes) + : Uint8Array.from(response.bytes); + if (response.truncated || bytes.byteLength > maxBytes) return null; + + try { + return strictUtf8Decoder.decode(bytes); + } catch { + return null; + } +} diff --git a/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx b/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx index e1bf0b043..85cfcae17 100644 --- a/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx +++ b/windows/tauri/src/features/layout/components/bottom-pane/bottom-pane.tsx @@ -245,7 +245,7 @@ const BottomPane = () => {
{ (state) => state.settings.footerTrailingItemsOrder, ); const filePathItem = useFooterFilePathItem(); - const branchItem = useFooterGitBranchItem(); const editorStatusItems = useFooterEditorStatusItems(); const footerLeadingItemsSource: Array | null> = [ filePathItem, - branchItem, ]; const footerLeadingItems = footerLeadingItemsSource.filter( (item): item is ChromeItem => item !== null, @@ -54,7 +51,7 @@ const Footer = () => { return ( diff --git a/windows/tauri/src/features/layout/components/main-layout.tsx b/windows/tauri/src/features/layout/components/main-layout.tsx index 2e710ed83..98250a52c 100644 --- a/windows/tauri/src/features/layout/components/main-layout.tsx +++ b/windows/tauri/src/features/layout/components/main-layout.tsx @@ -19,7 +19,6 @@ import { useWorkspaceTabsStore } from "@/features/window/stores/workspace-tabs.s import { useUIState } from "@/features/window/stores/ui-state.store"; import { toast } from "sonner"; import { useTranslation } from "@/i18n/locale-provider"; -import { cn } from "@/utils/cn"; import { frontendTrace } from "@/utils/frontend-trace"; import { recordStartupMilestone } from "@/features/bootstrap/startup-performance"; import { getInternalTabDragData } from "@/features/tabs/utils/internal-tab-drag"; @@ -264,7 +263,7 @@ export function MainLayout() { )} - + {rootFolderPath ? ( <> @@ -284,13 +283,7 @@ export function MainLayout() {
-
+
diff --git a/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts b/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts new file mode 100644 index 000000000..f5718d6a5 --- /dev/null +++ b/windows/tauri/src/features/layout/components/plugin-activity-rail.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test"; + +test("right activity rail opens the real singleton Extensions buffer", async () => { + const railSource = await Bun.file( + new URL("./plugin-activity-rail.tsx", import.meta.url), + ).text(); + const layoutSource = await Bun.file(new URL("./main-layout.tsx", import.meta.url)).text(); + + expect(railSource).toContain( + "const openExtensionsBuffer = useBufferStore.use.actions().openExtensionsBuffer;", + ); + expect(railSource).toContain('buffer.type === "extensions"'); + expect(railSource).toContain('const extensionsLabel = t("extensions.title");'); + expect(railSource).toContain("aria-pressed={isExtensionsActive}"); + expect(railSource).toContain("onClick={openExtensionsBuffer}"); + expect(railSource).not.toContain("useState"); + expect(layoutSource).toContain(""); +}); + +test("right activity rail reserves only the current Extensions entry", async () => { + const railSource = await Bun.file( + new URL("./plugin-activity-rail.tsx", import.meta.url), + ).text(); + const transparencyStyles = await Bun.file( + new URL("../../../styles/window-transparency.css", import.meta.url), + ).text(); + + expect(railSource).toContain( + 'className="lithe-plugin-activity-rail flex w-9.5 shrink-0 flex-col items-center rounded-r-xl border-border border-r bg-surface pt-1"', + ); + expect(railSource).toContain(''); + expect(railSource.match(/ - + ); } diff --git a/windows/tauri/src/features/layout/components/resizable-pane.tsx b/windows/tauri/src/features/layout/components/resizable-pane.tsx index f3ac8259d..8b66df2bb 100644 --- a/windows/tauri/src/features/layout/components/resizable-pane.tsx +++ b/windows/tauri/src/features/layout/components/resizable-pane.tsx @@ -185,6 +185,8 @@ export function ResizablePane({ className={cn( "lithe-resizable-pane relative flex h-full min-w-0 shrink-0 overflow-visible bg-transparent", hidden && "pointer-events-none", + !hidden && position === "left" && "mr-(--lithe-workbench-gap)", + !hidden && position === "right" && "ml-(--lithe-workbench-gap)", className, )} aria-hidden={hidden} @@ -198,11 +200,9 @@ export function ResizablePane({ >
{children} diff --git a/windows/tauri/src/features/layout/components/sidebar/main-sidebar.tsx b/windows/tauri/src/features/layout/components/sidebar/main-sidebar.tsx index 3b08492fe..5f03963a4 100644 --- a/windows/tauri/src/features/layout/components/sidebar/main-sidebar.tsx +++ b/windows/tauri/src/features/layout/components/sidebar/main-sidebar.tsx @@ -57,7 +57,6 @@ import { } from "@/ui/context-menu"; import { Spinner } from "@/ui/spinner"; import { - DatabaseIcon, EyeIcon, FilesIcon, FolderIcon, @@ -207,11 +206,6 @@ export const SidebarActivityRail = memo(({ expanded = false }: SidebarActivityRa }, ] : []), - { - id: "database", - label: t("workbench.database"), - icon: , - }, ...(coreFeatures.terminal ? [ { diff --git a/windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx b/windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx index 52123f568..58188a02d 100644 --- a/windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx +++ b/windows/tauri/src/features/layout/components/sidebar/sidebar-pane-selector.tsx @@ -14,7 +14,6 @@ import { useSettingsStore } from "@/features/settings/stores/settings.store"; import { SidebarListItem } from "@/ui/sidebar"; import { Tabs, TabsList, TabsTrigger } from "@/ui/tabs"; import { - DatabaseIcon, GearIcon, GitBranchIcon, GitGraphIcon, @@ -180,17 +179,6 @@ export const SidebarPaneSelector = ({ } satisfies SidebarPaneItem, ] : []), - { - id: "database", - label: showLabels ? t("workbench.database") : undefined, - icon: , - disabled: true, - ariaLabel: t("workbench.database"), - tooltip: { - content: BACKEND_UNAVAILABLE_TOOLTIP, - side: tooltipSide, - }, - }, ...(coreFeatures.terminal && onTerminalClick ? [ { diff --git a/windows/tauri/src/features/layout/components/workbench-panel-frame.test.ts b/windows/tauri/src/features/layout/components/workbench-panel-frame.test.ts new file mode 100644 index 000000000..6527743c2 --- /dev/null +++ b/windows/tauri/src/features/layout/components/workbench-panel-frame.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test"; + +test("workbench keeps the Project divider without outer horizontal or editor-to-rail seams", async () => { + const layoutSource = await Bun.file(new URL("./main-layout.tsx", import.meta.url)).text(); + const resizablePaneSource = await Bun.file( + new URL("./resizable-pane.tsx", import.meta.url), + ).text(); + const pluginRailSource = await Bun.file( + new URL("./plugin-activity-rail.tsx", import.meta.url), + ).text(); + const bottomPaneSource = await Bun.file( + new URL("./bottom-pane/bottom-pane.tsx", import.meta.url), + ).text(); + const footerSource = await Bun.file(new URL("./footer/footer.tsx", import.meta.url)).text(); + const titleBarSource = await Bun.file( + new URL("../../window/components/title-bar/title-bar.tsx", import.meta.url), + ).text(); + + expect(layoutSource).toContain( + 'className="lithe-glass-island relative min-h-0 flex-1 overflow-hidden rounded-xl border-border border-l bg-background"', + ); + expect(resizablePaneSource).toContain( + '!hidden && position === "left" && "mr-(--lithe-workbench-gap)"', + ); + expect(resizablePaneSource).toContain( + '!hidden && position === "right" && "ml-(--lithe-workbench-gap)"', + ); + expect(resizablePaneSource).toContain( + '!hidden && "rounded-xl border-border border-x"', + ); + expect(pluginRailSource).toContain( + 'className="lithe-plugin-activity-rail flex w-9.5 shrink-0 flex-col items-center rounded-r-xl border-border border-r bg-surface pt-1"', + ); + expect(pluginRailSource).not.toContain('ml-(--lithe-workbench-gap)'); + expect(pluginRailSource).not.toContain("border-l"); + expect(bottomPaneSource).toContain( + 'rounded-xl border-border/70 border-t border-l bg-background', + ); + expect(titleBarSource).toContain( + 'gap-(--lithe-chrome-gap) bg-surface px-(--lithe-chrome-padding-inline)', + ); + expect(titleBarSource).not.toContain( + 'gap-(--lithe-chrome-gap) border-border border-b bg-surface', + ); + expect(footerSource).toContain( + 'className="lithe-footer-bar relative z-20 justify-between gap-2 bg-surface"', + ); + expect(footerSource).not.toContain("border-t"); + expect(resizablePaneSource).toContain('const totalWidth = hidden ? "0px" : `${width}px`;'); + expect(resizablePaneSource).toContain('contentEl.style.width = `${currentWidth}px`;'); +}); diff --git a/windows/tauri/src/features/layout/config/item-order.test.ts b/windows/tauri/src/features/layout/config/item-order.test.ts index a6c33adc3..c53e4b458 100644 --- a/windows/tauri/src/features/layout/config/item-order.test.ts +++ b/windows/tauri/src/features/layout/config/item-order.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { FOOTER_TRAILING_ITEM_IDS, + SIDEBAR_ACTIVITY_ITEM_IDS, SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS, normalizeItemOrder, } from "./item-order"; @@ -22,6 +23,10 @@ describe("footer item order", () => { }); describe("sidebar activity order", () => { + test("does not expose an unavailable Database placeholder", () => { + expect([...SIDEBAR_ACTIVITY_ITEM_IDS]).not.toContain("database"); + }); + test("places Run, Terminal, Diagnostics, Git Log, then Settings", () => { expect([...SIDEBAR_BOTTOM_ACTIVITY_ITEM_IDS]).toEqual([ "run", diff --git a/windows/tauri/src/features/layout/config/item-order.ts b/windows/tauri/src/features/layout/config/item-order.ts index 11f2ca4fd..6c05b4e89 100644 --- a/windows/tauri/src/features/layout/config/item-order.ts +++ b/windows/tauri/src/features/layout/config/item-order.ts @@ -3,7 +3,6 @@ export const SIDEBAR_ACTIVITY_ITEM_IDS = [ "files", "git", "search", - "database", "run", "terminal", "diagnostics", diff --git a/windows/tauri/src/features/settings/config/default-settings.ts b/windows/tauri/src/features/settings/config/default-settings.ts index b34287be3..1e1853e19 100644 --- a/windows/tauri/src/features/settings/config/default-settings.ts +++ b/windows/tauri/src/features/settings/config/default-settings.ts @@ -64,7 +64,7 @@ export const defaultSettings: Settings = { // UI uiFontFamily: DEFAULT_UI_FONT_FAMILY, uiFontSize: UI_FONT_SIZE_DEFAULT, - displayLanguage: "en-US", + displayLanguage: "zh-CN", reduceMotion: false, showStatusBar: true, showTabIcons: true, @@ -72,7 +72,7 @@ export const defaultSettings: Settings = { windowChromeDensity: "focused", // Theme theme: "lithe-dark", - iconTheme: "lithe-icons", + iconTheme: "idea-icons", syncSystemTheme: false, autoThemeLight: "lithe-light", autoThemeDark: "lithe-dark", diff --git a/windows/tauri/src/features/settings/config/typography-defaults.ts b/windows/tauri/src/features/settings/config/typography-defaults.ts index 84ee4605f..61e4036de 100644 --- a/windows/tauri/src/features/settings/config/typography-defaults.ts +++ b/windows/tauri/src/features/settings/config/typography-defaults.ts @@ -1,4 +1,4 @@ -export const DEFAULT_UI_FONT_FAMILY = "Geist Sans"; +export const DEFAULT_UI_FONT_FAMILY = "Microsoft YaHei UI"; export const DEFAULT_MONO_FONT_FAMILY = "Geist Mono"; const DEFAULT_MONO_FONT_FALLBACK = @@ -11,8 +11,7 @@ const WINDOWS_SANS_FONT_FALLBACK = '"Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Arial, sans-serif'; export const DEFAULT_CODE_FONT_SIZE = 14; -const DEFAULT_UI_FONT_SIZE_OFFSET = 1; -export const DEFAULT_UI_FONT_SIZE = DEFAULT_CODE_FONT_SIZE + DEFAULT_UI_FONT_SIZE_OFFSET; +export const DEFAULT_UI_FONT_SIZE = 13; export function getTypographyFontFallbacks(isWindows: boolean) { return { diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts index f4f5f6df6..dd493e53e 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.test.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.test.ts @@ -29,3 +29,54 @@ describe("JDTLS JDK setting normalization", () => { expect(normalizeSettings(settings).jdtlsJavaHomePath).toBe(""); }); }); + +describe("IDEA file icon theme normalization", () => { + test("uses IDEA Icons for new settings", () => { + expect(getDefaultSettingsSnapshot().iconTheme).toBe("idea-icons"); + }); + + test("migrates current and legacy Lithe icon theme ids", () => { + for (const iconTheme of [ + "lithe-icons", + "lithe-icons-dimmed", + "lithe-icons-light", + "lithe-file-icons", + "lithe-file-icons-dark", + "lithe-file-icons-light", + ]) { + expect(normalizeSettingValue("iconTheme", iconTheme)).toBe("idea-icons"); + } + }); +}); + +describe("Windows New UI defaults", () => { + test("uses Chinese and the native CJK UI typography for new settings", () => { + const settings = getDefaultSettingsSnapshot(); + + expect(settings.displayLanguage).toBe("zh-CN"); + expect(settings.uiFontFamily).toBe("Microsoft YaHei UI"); + expect(settings.uiFontSize).toBe(13); + }); + + test("migrates the previous bundled UI typography as a pair", () => { + const settings = getDefaultSettingsSnapshot(); + settings.uiFontFamily = "Geist Sans"; + settings.uiFontSize = 15; + + const normalized = normalizeSettings(settings); + + expect(normalized.uiFontFamily).toBe("Microsoft YaHei UI"); + expect(normalized.uiFontSize).toBe(13); + }); + + test("preserves an explicitly customized typography pair", () => { + const settings = getDefaultSettingsSnapshot(); + settings.uiFontFamily = "Geist Sans"; + settings.uiFontSize = 14; + + const normalized = normalizeSettings(settings); + + expect(normalized.uiFontFamily).toBe("Geist Sans"); + expect(normalized.uiFontSize).toBe(14); + }); +}); diff --git a/windows/tauri/src/features/settings/lib/settings-normalization.ts b/windows/tauri/src/features/settings/lib/settings-normalization.ts index 767c0b74e..b396f119c 100644 --- a/windows/tauri/src/features/settings/lib/settings-normalization.ts +++ b/windows/tauri/src/features/settings/lib/settings-normalization.ts @@ -11,6 +11,7 @@ import { import { DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, + DEFAULT_UI_FONT_SIZE, } from "@/features/settings/config/typography-defaults"; import { normalizeConfiguredFontFamily } from "@/features/settings/lib/font-family-resolution"; import { @@ -95,6 +96,9 @@ const AI_MODEL_MIGRATIONS: Record> = { }, }; +const LEGACY_DEFAULT_UI_FONT_FAMILY = "Geist Sans"; +const LEGACY_DEFAULT_UI_FONT_SIZE = 15; + const AI_AUTOCOMPLETE_MODEL_MIGRATIONS: Record = { "google/gemini-2.5-flash-lite": "google/gemini-3.1-flash-lite", }; @@ -207,13 +211,14 @@ function normalizeStringList(value: unknown): string[] { function normalizeIconTheme(value: string): string { if ( + value === "lithe-icons" || value === "lithe-icons-dimmed" || value === "lithe-icons-light" || value === "lithe-file-icons" || value === "lithe-file-icons-dark" || value === "lithe-file-icons-light" ) { - return "lithe-icons"; + return "idea-icons"; } if (value === "colorful-material" || value === "seti") { @@ -460,6 +465,14 @@ export function normalizeSettings(settings: Settings): Settings { normalizedSettings.gitSidebarTabOrder = ["changes", "history"]; } + if ( + normalizedSettings.uiFontFamily === LEGACY_DEFAULT_UI_FONT_FAMILY && + normalizedSettings.uiFontSize === LEGACY_DEFAULT_UI_FONT_SIZE + ) { + normalizedSettings.uiFontFamily = DEFAULT_UI_FONT_FAMILY; + normalizedSettings.uiFontSize = DEFAULT_UI_FONT_SIZE; + } + normalizedSettings.uiFontSize = normalizeUiFontSize(normalizedSettings.uiFontSize); normalizedSettings.fontFamily = normalizeConfiguredFontFamily( normalizedSettings.fontFamily, diff --git a/windows/tauri/src/features/sidebar/components/sidebar-tree.tsx b/windows/tauri/src/features/sidebar/components/sidebar-tree.tsx index 10803134b..09d5f11ef 100644 --- a/windows/tauri/src/features/sidebar/components/sidebar-tree.tsx +++ b/windows/tauri/src/features/sidebar/components/sidebar-tree.tsx @@ -237,7 +237,7 @@ export const SidebarTreeRow = forwardRef data-depth={depth} tabIndex={tabIndex} className={cn( - "file-tree-row font-sans ui-text-sm flex w-full min-w-0 flex-1 select-none items-center whitespace-nowrap rounded-lg border border-transparent bg-transparent text-left text-foreground outline-none transition-colors duration-(--app-duration-fast) ease-(--app-ease-smooth) hover:bg-accent focus-visible:border-primary/40 gap-1.5 px-1.5 py-1 leading-row", + "file-tree-row font-sans ui-text-chrome flex w-full min-w-0 flex-1 select-none items-center whitespace-nowrap rounded-(--lithe-chrome-radius) border border-transparent bg-transparent text-left text-foreground outline-none transition-colors duration-(--app-duration-fast) ease-(--app-ease-smooth) hover:bg-accent focus-visible:border-primary/40 gap-1.5 px-1.5 py-1 leading-row", active && "bg-selected", action && "pr-0", className, diff --git a/windows/tauri/src/features/sidebar/styles/sidebar-tree.css b/windows/tauri/src/features/sidebar/styles/sidebar-tree.css index 5427cc093..53d601617 100644 --- a/windows/tauri/src/features/sidebar/styles/sidebar-tree.css +++ b/windows/tauri/src/features/sidebar/styles/sidebar-tree.css @@ -11,7 +11,7 @@ position: absolute; inset: 0; z-index: 0; - border-radius: var(--radius-lg); + border-radius: var(--lithe-chrome-radius); background-color: transparent; content: ""; pointer-events: none; diff --git a/windows/tauri/src/features/tabs/components/tab-bar-item.test.ts b/windows/tauri/src/features/tabs/components/tab-bar-item.test.ts new file mode 100644 index 000000000..f435ca141 --- /dev/null +++ b/windows/tauri/src/features/tabs/components/tab-bar-item.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; + +test("editor tabs pass Java declaration semantics to the active icon theme", async () => { + const source = await Bun.file(new URL("./tab-bar-item.tsx", import.meta.url)).text(); + + expect(source).toContain( + 'import { useJavaFileIconKind } from "@/extensions/icon-themes/hooks/use-java-file-icon-kind";', + ); + expect(source).toContain('buffer.type === "editor" && !buffer.isVirtual'); + expect(source).toContain("semanticKind={javaSemanticKind}"); +}); diff --git a/windows/tauri/src/features/tabs/components/tab-bar-item.tsx b/windows/tauri/src/features/tabs/components/tab-bar-item.tsx index 8cd5ed796..b1258fccd 100644 --- a/windows/tauri/src/features/tabs/components/tab-bar-item.tsx +++ b/windows/tauri/src/features/tabs/components/tab-bar-item.tsx @@ -16,6 +16,7 @@ import { import { memo, useCallback, useEffect, useState } from "react"; import type { RefCallback } from "react"; import { ThemedFileIcon } from "@/extensions/icon-themes/components/themed-file-icon"; +import { useJavaFileIconKind } from "@/extensions/icon-themes/hooks/use-java-file-icon-kind"; import { getSingletonToolBufferTitleKey } from "@/features/panes/constants/tool-buffers"; import type { PaneContent } from "@/features/panes/types/pane-content.types"; import { shouldShowTabCloseButton } from "@/features/settings/lib/ui-preferences"; @@ -75,6 +76,12 @@ const TabBarItem = memo(function TabBarItem({ const [faviconError, setFaviconError] = useState(false); const [avatarError, setAvatarError] = useState(false); const showTabIcons = useSettingsStore((state) => state.settings.showTabIcons); + const javaSemanticKind = useJavaFileIconKind( + buffer.path, + buffer.name, + false, + showTabIcons && buffer.type === "editor" && !buffer.isVirtual, + ); const tabCloseButtonVisibility = useSettingsStore( (state) => state.settings.tabCloseButtonVisibility, ); @@ -239,6 +246,7 @@ const TabBarItem = memo(function TabBarItem({ )} diff --git a/windows/tauri/src/features/tabs/components/tab-bar.tsx b/windows/tauri/src/features/tabs/components/tab-bar.tsx index ac74a6e7e..914e882db 100644 --- a/windows/tauri/src/features/tabs/components/tab-bar.tsx +++ b/windows/tauri/src/features/tabs/components/tab-bar.tsx @@ -10,6 +10,7 @@ import { SidebarSimpleIcon as PanelLeftClose, } from "@/ui/icons"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useJavaFileIconCacheLifecycle } from "@/extensions/icon-themes/hooks/use-java-file-icon-kind"; import { useBufferStore } from "@/features/editor/stores/buffer.store"; import { useJumpListStore } from "@/features/editor/stores/jump-list.store"; import { useEditorStateStore } from "@/features/editor/stores/state.store"; @@ -60,6 +61,7 @@ const TabBar = ({ onTabClick: externalTabClick, disablePaneActions = false, }: TabBarProps) => { + useJavaFileIconCacheLifecycle(); const { t } = useTranslation(); // Get everything from stores const paneRoot = usePaneStore.use.root(); diff --git a/windows/tauri/src/features/terminal/components/terminal-container.tsx b/windows/tauri/src/features/terminal/components/terminal-container.tsx index fe079ef55..881af0456 100644 --- a/windows/tauri/src/features/terminal/components/terminal-container.tsx +++ b/windows/tauri/src/features/terminal/components/terminal-container.tsx @@ -8,6 +8,10 @@ import { useTerminalProfilesStore } from "@/features/terminal/stores/profiles.st import { useTerminalStore } from "@/features/terminal/stores/terminal.store"; import { useTerminalShellsStore } from "@/features/terminal/stores/shells.store"; import type { TerminalSplitDirection } from "@/features/terminal/types/terminal.types"; +import { + openNewTerminalFromGlobalEntry, + shouldAutoHideTerminalPane, +} from "@/features/terminal/utils/terminal-pane-visibility"; import { resolveTerminalLaunch, SYSTEM_DEFAULT_PROFILE_ID, @@ -85,7 +89,7 @@ const TerminalContainer = ({ [originalCloseTerminal], ); - const hasInitializedRef = useRef(false); + const previousTerminalCountRef = useRef(terminals.length); const wasVisibleRef = useRef(false); const workspaceDirectoryRef = useRef(currentDirectory); const terminalSessionRefs = useRef void; showSearch: () => void }>>( @@ -99,7 +103,7 @@ const TerminalContainer = ({ } workspaceDirectoryRef.current = currentDirectory; - hasInitializedRef.current = terminals.length > 0; + previousTerminalCountRef.current = terminals.length; wasVisibleRef.current = false; }, [currentDirectory, terminals.length]); const registerTerminalFocus = useUIState((state) => state.registerTerminalFocus); @@ -190,12 +194,23 @@ const TerminalContainer = ({ }; }, []); - // Auto-close bottom pane when all terminals are closed + // Collapse only on a visible terminal pane's transition from live tabs to none. + // An already-empty pane is allowed to remain visible long enough to create its first terminal. useEffect(() => { - if (terminals.length === 0 && hasInitializedRef.current) { + const previousTerminalCount = previousTerminalCountRef.current; + previousTerminalCountRef.current = terminals.length; + + if ( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab, + isBottomPaneVisible, + previousTerminalCount, + terminalCount: terminals.length, + }) + ) { setIsBottomPaneVisible(false); } - }, [terminals.length, setIsBottomPaneVisible]); + }, [bottomPaneActiveTab, isBottomPaneVisible, terminals.length, setIsBottomPaneVisible]); const handleTabClick = useCallback( (terminalId: string) => { @@ -508,7 +523,11 @@ const TerminalContainer = ({ useEffect(() => { const handleNewTerminalEvent = () => { - handleNewTerminal(); + openNewTerminalFromGlobalEntry({ + createTerminal: handleNewTerminal, + setBottomPaneActiveTab, + setIsBottomPaneVisible, + }); }; const handleDetachTerminalToBuffer = (event: Event) => { @@ -521,7 +540,6 @@ const TerminalContainer = ({ const handleEnsureTerminalSession = () => { if (terminals.length === 0) { - hasInitializedRef.current = true; handleNewTerminal(); return; } @@ -558,6 +576,8 @@ const TerminalContainer = ({ focusActiveTerminal, handleNewTerminal, setActiveTerminal, + setBottomPaneActiveTab, + setIsBottomPaneVisible, handleSplitView, closeTerminal, ]); @@ -568,7 +588,6 @@ const TerminalContainer = ({ const justBecameVisible = isTerminalVisible && !wasVisibleRef.current; if (justBecameVisible && terminals.length === 0) { - hasInitializedRef.current = true; handleNewTerminal(); } diff --git a/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.test.ts b/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.test.ts new file mode 100644 index 000000000..ba269046a --- /dev/null +++ b/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "bun:test"; +import { + openNewTerminalFromGlobalEntry, + shouldAutoHideTerminalPane, +} from "./terminal-pane-visibility"; + +describe("terminal pane auto-hide", () => { + test("hides the visible terminal pane after its last terminal closes", () => { + expect( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab: "terminal", + isBottomPaneVisible: true, + previousTerminalCount: 1, + terminalCount: 0, + }), + ).toBe(true); + }); + + test("keeps an already-empty terminal pane open so a new terminal can be created", () => { + expect( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab: "terminal", + isBottomPaneVisible: true, + previousTerminalCount: 0, + terminalCount: 0, + }), + ).toBe(false); + }); + + test("does not hide a terminal pane that is already hidden", () => { + expect( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab: "terminal", + isBottomPaneVisible: false, + previousTerminalCount: 1, + terminalCount: 0, + }), + ).toBe(false); + }); + + test("does not hide another active bottom tool window when terminals close", () => { + for (const bottomPaneActiveTab of ["run", "gitLog", "debugger", "buffers"]) { + expect( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab, + isBottomPaneVisible: true, + previousTerminalCount: 1, + terminalCount: 0, + }), + ).toBe(false); + } + }); + + test("keeps the terminal pane visible while another terminal remains", () => { + expect( + shouldAutoHideTerminalPane({ + bottomPaneActiveTab: "terminal", + isBottomPaneVisible: true, + previousTerminalCount: 2, + terminalCount: 1, + }), + ).toBe(false); + }); + + test("global new-terminal entry shows the terminal pane and creates exactly one terminal", () => { + const calls: string[] = []; + + openNewTerminalFromGlobalEntry({ + createTerminal: () => calls.push("create"), + setBottomPaneActiveTab: (tab) => calls.push(`tab:${tab}`), + setIsBottomPaneVisible: (visible) => calls.push(`visible:${visible}`), + }); + + expect(calls).toEqual(["tab:terminal", "visible:true", "create"]); + }); +}); diff --git a/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.ts b/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.ts new file mode 100644 index 000000000..bdf2336a6 --- /dev/null +++ b/windows/tauri/src/features/terminal/utils/terminal-pane-visibility.ts @@ -0,0 +1,36 @@ +interface TerminalPaneVisibilityState { + bottomPaneActiveTab: string; + isBottomPaneVisible: boolean; + previousTerminalCount: number; + terminalCount: number; +} + +interface NewTerminalEntryActions { + createTerminal: () => void; + setBottomPaneActiveTab: (tab: "terminal") => void; + setIsBottomPaneVisible: (visible: true) => void; +} + +export function shouldAutoHideTerminalPane({ + bottomPaneActiveTab, + isBottomPaneVisible, + previousTerminalCount, + terminalCount, +}: TerminalPaneVisibilityState): boolean { + return ( + isBottomPaneVisible && + bottomPaneActiveTab === "terminal" && + previousTerminalCount > 0 && + terminalCount === 0 + ); +} + +export function openNewTerminalFromGlobalEntry({ + createTerminal, + setBottomPaneActiveTab, + setIsBottomPaneVisible, +}: NewTerminalEntryActions): void { + setBottomPaneActiveTab("terminal"); + setIsBottomPaneVisible(true); + createTerminal(); +} diff --git a/windows/tauri/src/features/window/components/project-tab-bar.tsx b/windows/tauri/src/features/window/components/project-tab-bar.tsx index 19d7bb6f2..74e1cf859 100644 --- a/windows/tauri/src/features/window/components/project-tab-bar.tsx +++ b/windows/tauri/src/features/window/components/project-tab-bar.tsx @@ -5,9 +5,13 @@ import { useWorkspaceTabsStore } from "@/features/window/stores/workspace-tabs.s import { Button } from "@/ui/button"; import { FolderIcon, XIcon as X } from "@/ui/icons"; import { cn } from "@/utils/cn"; -import { getProjectTabBarItems } from "../utils/project-tab-bar-model"; +import { getProjectTabBarItems, shouldShowProjectTabBar } from "../utils/project-tab-bar-model"; -export function ProjectTabBar() { +interface ProjectTabBarProps { + hideWhenSingle?: boolean; +} + +export function ProjectTabBar({ hideWhenSingle = false }: ProjectTabBarProps) { const { t } = useTranslation(); const [closingProjectId, setClosingProjectId] = useState(null); const projectTabs = useWorkspaceTabsStore.use.projectTabs(); @@ -28,11 +32,11 @@ export function ProjectTabBar() { } }; - if (projects.length === 0) return null; + if (!shouldShowProjectTabBar(projects.length, hideWhenSingle)) return null; return (