feat(project-tree): enrich JVM project view icons (#512)

Project rows only showed generic folder and filename icons, so JVM modules, packages and declarations looked alike.

Rows now show Maven and Gradle project and module icons, packages, Spring configuration and Boot application classes, runnable classes, test classes, Java and Kotlin declaration types, and generated, compiled and javadoc output folders. Editor tabs, Goto File and Recent Files use the same resolver. Types come from a bounded syntax parse of visible files, served locally and over remote RPC.

Spring stereotypes keep their ordinary type icons, and .class files stay binary.

Fixes #347
This commit is contained in:
l0ng-ai
2026-10-31 10:40:24 +08:00
committed by GitHub
parent 504d8592d2
commit 9d670e6262
50 changed files with 1279 additions and 20 deletions
+3 -2
View File
@@ -12,12 +12,13 @@ icon set, copied unmodified (some renamed) from JetBrains' open-source repositor
`python/python-requirements` (`fileTypes/requirements`); `python/pipenv`
(`fileTypes/pipenv`); `python/python-pyproject` (`expui/toml.svg` →
`fileTypes/pyprojectToml`); `plugins/kotlin/base` (`fileTypes/kotlin`,
`kotlinScript`, `kotlinGradleScript`); `java/openapi` (`fileTypes/maven`);
`kotlinScript`, `kotlinGradleScript`, `icons/expui/objectKotlin` → `kotlin/objectKotlin`); `java/openapi` (`fileTypes/maven`);
`plugins/markdown` (`fileTypes/markdown`); `plugins/toml` (`fileTypes/toml`);
`plugins/terminal` (`toolwindows/terminal`); `plugins/maven`
(`images/expui/toolwindow/maven` → `toolwindows/maven`, `images/expui/build/maven*`
→ `maven/`); `plugins/gradle` (`icons/expui/gradle` → `toolwindows/gradle`,
`icons/expui/gradleLoadChanges` → `build/gradleLoadChanges`);
`icons/expui/gradleLoadChanges` → `build/gradleLoadChanges`,
`icons/expui/gradle`, `icons/gradleSubproject` → `gradle/`);
`platform/external-system-impl` (`icons/newui/task` → `externalSystem/task`).
Copyright 2000-2026 JetBrains s.r.o. and contributors. Apache License 2.0 (below).
- [intellij-plugins](https://github.com/JetBrains/intellij-plugins) (each plugin
+102
View File
@@ -0,0 +1,102 @@
//! Bounded, read-only JVM presentation for visible files, also served remotely.
use crate::Host;
use ide_core::{CancellationToken, IdeError, IdeResult, LanguageId};
use ide_syntax::{JvmPresentation, Snapshot};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{
collections::HashMap,
fs::File,
io::Read,
path::PathBuf,
sync::{Mutex, OnceLock},
time::SystemTime,
};
pub const PREFIX_BYTES: u64 = 64 * 1024;
const MAX_BATCH: usize = 64;
const MAX_CACHE: usize = 4096;
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct JvmFilePresentation {
pub path: PathBuf,
pub presentation: JvmPresentation,
}
#[derive(Clone)]
struct Cached {
modified: SystemTime,
size: u64,
presentation: JvmPresentation,
}
fn prefix(path: &PathBuf) -> std::io::Result<String> {
let mut bytes = Vec::new();
File::open(path)?.take(PREFIX_BYTES).read_to_end(&mut bytes)?;
// A truncated final UTF-8 character must not invalidate the entire prefix.
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
pub async fn jvm_file_presentations(
host: &Host,
window: &str,
paths: Vec<PathBuf>,
) -> IdeResult<Vec<JvmFilePresentation>> {
if paths.len() > MAX_BATCH {
return Err(IdeError::invalid("at most 64 JVM files per batch"));
}
let project = host.project(window)?;
host.blocking(move || {
static CACHE: OnceLock<Mutex<HashMap<PathBuf, Cached>>> = OnceLock::new();
let cache = CACHE.get_or_init(Default::default);
let mut results = Vec::new();
for path in paths {
let language = match path.extension().and_then(|e| e.to_str()) {
Some("java") => LanguageId::new("java"),
Some("kt") => LanguageId::new("kotlin"),
_ => continue,
};
let Ok(meta) = path.metadata() else { continue };
if !meta.is_file() {
continue;
}
let Ok(modified) = meta.modified() else { continue };
let hit =
cache.lock().unwrap().get(&path).filter(|c| c.modified == modified && c.size == meta.len()).cloned();
let presentation = if let Some(hit) = hit {
hit.presentation
} else {
let Ok(source) = prefix(&path) else { continue };
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default();
let presentation = project.structure.jvm_presentation(
&Snapshot::text(&language, &source),
stem,
&CancellationToken::new(),
)?;
let mut cache = cache.lock().unwrap();
if cache.len() >= MAX_CACHE {
cache.clear();
}
cache.insert(path.clone(), Cached { modified, size: meta.len(), presentation: presentation.clone() });
presentation
};
results.push(JvmFilePresentation { path, presentation });
}
Ok(results)
})
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn huge_files_read_only_a_prefix() {
let path = std::env::temp_dir().join(format!("openidea-icon-prefix-{}.java", std::process::id()));
let file = File::create(&path).unwrap();
file.set_len(100 * 1024 * 1024).unwrap();
assert_eq!(prefix(&path).unwrap().len(), PREFIX_BYTES as usize);
std::fs::remove_file(path).unwrap();
}
}
+1
View File
@@ -13,6 +13,7 @@ pub mod env;
pub mod fs;
pub mod history;
pub mod java;
pub mod jvm_icons;
pub mod lsp;
pub mod nav;
pub mod notebook;
+3
View File
@@ -148,6 +148,9 @@ impl Dispatcher {
c.host.close_project(&c.window);
Ok(())
})
.add("jvm_file_presentations", |c| async move {
ops::jvm_icons::jvm_file_presentations(&c.host, &c.window, c.arg("paths")?).await
})
.add("project_roots", |c| async move { ops::project::roots(&c.host, &c.window) })
.add("settings_get", |c| async move { Ok(ops::project::settings(&c.host, &c.window)) })
.add("settings_set", |c| async move {
+16
View File
@@ -81,6 +81,22 @@ async fn serves_projects_files_documents_search_and_runs() {
let entries = invoke(&peer, "fs_read_dir", json!({ "path": project })).await.unwrap();
assert!(entries.as_array().unwrap().iter().any(|e| e["name"] == "src"), "{entries}");
// JVM icons use the same read-only protocol as local windows.
let java = project.join("src/Foo.java");
let kotlin = project.join("src/Singleton.kt");
std::fs::write(&java, "abstract class Foo {}").unwrap();
std::fs::write(&kotlin, "object Singleton {}").unwrap();
let icons = invoke(&peer, "jvm_file_presentations", json!({ "paths": [java, kotlin] })).await.unwrap();
assert_eq!(icons[0]["presentation"]["kind"], "abstractClass");
assert_eq!(icons[1]["presentation"]["kind"], "object");
// Replacing a file invalidates its cached metadata, including same-size writes.
std::fs::write(&java, "interface Foo {} ").unwrap();
std::fs::File::open(&java).unwrap().set_modified(std::time::SystemTime::UNIX_EPOCH).unwrap();
let icons = invoke(&peer, "jvm_file_presentations", json!({ "paths": [java] })).await.unwrap();
assert_eq!(icons[0]["presentation"]["kind"], "interface");
let oversized = invoke(&peer, "jvm_file_presentations", json!({ "paths": vec![java.clone(); 65] })).await;
assert!(oversized.is_err());
// Edit and save a document.
let file = project.join("src/main.py");
let doc = invoke(&peer, "doc_open", json!({ "path": file })).await.unwrap();
+189
View File
@@ -0,0 +1,189 @@
//! Syntactic presentation of the primary top-level JVM declaration.
use crate::{Snapshot, StructureResult, StructureService};
use ide_core::CancellationToken;
use serde::{Deserialize, Serialize};
use specta::Type;
use tree_sitter::Node;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub enum JvmTypeKind {
Class,
AbstractClass,
Interface,
Enum,
Record,
Annotation,
Exception,
Object,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct JvmPresentation {
pub kind: Option<JvmTypeKind>,
pub spring_boot: bool,
pub runnable: bool,
}
fn text<'a>(node: Node, source: &'a str) -> &'a str {
source.get(node.byte_range()).unwrap_or_default()
}
fn children(node: Node) -> Vec<Node> {
node.children(&mut node.walk()).collect()
}
fn descendant(node: Node<'_>, predicate: &impl Fn(Node<'_>) -> bool) -> bool {
predicate(node) || node.children(&mut node.walk()).any(|n| descendant(n, predicate))
}
fn keyword(node: Node, source: &str, word: &str) -> bool {
descendant(node, &|n| {
n.child_count() == 0 && (n.kind() == word || n.kind().ends_with("_modifier") && text(n, source) == word)
})
}
fn annotation(node: Node, source: &str, name: &str) -> bool {
node.named_children(&mut node.walk()).any(|n| {
if !matches!(n.kind(), "annotation" | "marker_annotation") {
return false;
}
if let Some(id) = n.child_by_field_name("name") {
return text(id, source).rsplit('.').next() == Some(name);
}
// Kotlin: @ -> constructor_invocation -> user_type (or type -> user_type).
let mut current = n;
while let Some(first) = current.named_children(&mut current.walk()).next() {
if matches!(first.kind(), "use_site_target") {
return false;
}
if first.kind() == "user_type" {
let ids: Vec<_> =
first.named_children(&mut first.walk()).filter(|c| c.kind() == "identifier").collect();
return ids.last().is_some_and(|id| text(*id, source) == name);
}
current = first;
}
false
})
}
fn exception_base(node: Node, source: &str) -> bool {
if matches!(node.kind(), "type_arguments" | "value_arguments" | "explicit_delegation") {
return false;
}
if matches!(node.kind(), "type_identifier" | "user_type" | "scoped_type_identifier") {
let name = if node.kind() == "user_type" || node.kind() == "scoped_type_identifier" {
node.named_children(&mut node.walk())
.filter(|n| matches!(n.kind(), "identifier" | "type_identifier"))
.last()
.map(|n| text(n, source))
.unwrap_or_default()
} else {
text(node, source)
};
return name.ends_with("Exception") || name.ends_with("Error") || name == "Throwable";
}
node.named_children(&mut node.walk()).any(|n| exception_base(n, source))
}
fn members(node: Node<'_>) -> Vec<Node<'_>> {
let mut out = Vec::new();
for child in node.named_children(&mut node.walk()) {
if matches!(child.kind(), "method_declaration" | "function_declaration") {
out.push(child);
}
if matches!(child.kind(), "companion_object" | "class_body" | "enum_body_declarations") {
out.extend(members(child));
}
}
out
}
fn is_type(node: Node) -> bool {
matches!(
node.kind(),
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
| "annotation_type_declaration"
| "object_declaration"
)
}
impl StructureService {
/// Only top-level declarations participate; the filename-matching declaration
/// wins, otherwise the first type does. Strings/comments never supply evidence.
pub fn jvm_presentation(
&self,
snap: &Snapshot,
stem: &str,
cancel: &CancellationToken,
) -> StructureResult<JvmPresentation> {
self.run(snap, cancel, |cx| {
if !matches!(cx.grammar, "java" | "kotlin") {
return Ok(JvmPresentation::default());
}
let top = cx.tree.root_node();
let types: Vec<_> = top.named_children(&mut top.walk()).filter(|n| is_type(*n)).collect();
let Some(node) = types
.iter()
.find(|n| n.child_by_field_name("name").is_some_and(|id| text(id, snap.text) == stem))
.or(types.first())
.copied()
else {
return Ok(JvmPresentation::default());
};
let parts = children(node);
let modifiers = parts.iter().find(|n| n.kind() == "modifiers").copied();
let has = |word| modifiers.is_some_and(|m| keyword(m, snap.text, word));
let supertype = parts.iter().find(|n| matches!(n.kind(), "superclass" | "delegation_specifiers"));
let exception = supertype.is_some_and(|s| exception_base(*s, snap.text));
let kind = match node.kind() {
"annotation_type_declaration" => JvmTypeKind::Annotation,
"interface_declaration" => JvmTypeKind::Interface,
"enum_declaration" => JvmTypeKind::Enum,
"record_declaration" => JvmTypeKind::Record,
"object_declaration" => JvmTypeKind::Object,
_ if has("annotation") => JvmTypeKind::Annotation,
_ if has("enum") => JvmTypeKind::Enum,
_ if parts.iter().any(|n| n.kind() == "interface") => JvmTypeKind::Interface,
_ if exception => JvmTypeKind::Exception,
_ if has("abstract") => JvmTypeKind::AbstractClass,
_ => JvmTypeKind::Class,
};
let spring_boot = modifiers.is_some_and(|m| annotation(m, snap.text, "SpringBootApplication"));
let body = parts.iter().find(|n| n.kind() == "class_body");
let runnable = body.is_some_and(|b| {
members(*b).into_iter().any(|n| {
if !matches!(n.kind(), "method_declaration" | "function_declaration")
|| !n.child_by_field_name("name").is_some_and(|id| text(id, snap.text) == "main")
{
return false;
}
if cx.grammar == "kotlin" {
return children(n)
.iter()
.any(|m| m.kind() == "modifiers" && annotation(*m, snap.text, "JvmStatic"));
}
let modifiers = children(n).into_iter().find(|m| m.kind() == "modifiers");
let params = n.child_by_field_name("parameters");
modifiers.is_some_and(|m| keyword(m, snap.text, "static") && keyword(m, snap.text, "public"))
&& n.child_by_field_name("type").is_some_and(|t| text(t, snap.text) == "void")
&& params.is_some_and(|p| {
let args: Vec<_> = p.named_children(&mut p.walk()).collect();
args.len() == 1
&& descendant(args[0], &|t| {
t.kind() == "type_identifier"
&& matches!(text(t, snap.text), "String" | "java.lang.String")
})
&& (keyword(args[0], snap.text, "[") || keyword(args[0], snap.text, "..."))
})
})
});
Ok(JvmPresentation { kind: Some(kind), spring_boot, runnable })
})
}
}
+2
View File
@@ -19,6 +19,7 @@
//! the query loops honour the [`CancellationToken`].
mod identifiers;
mod jvm;
mod kinds;
mod lines;
mod locals;
@@ -28,6 +29,7 @@ mod types;
#[cfg(test)]
mod tests;
pub use jvm::{JvmPresentation, JvmTypeKind};
pub use types::*;
use ide_core::{CancellationToken, DocId, IdeError, LanguageId, Pos, Range};
+78
View File
@@ -0,0 +1,78 @@
use super::*;
use crate::{JvmPresentation, JvmTypeKind::*};
fn presentation(language: &str, text: &str) -> JvmPresentation {
service().jvm_presentation(&Snapshot::text(&lang(language), text), "Foo", &none()).unwrap()
}
#[test]
fn jvm_declaration_kinds() {
for (language, source, kind) in [
("java", "public class Foo {}", Class),
("java", "abstract class Foo {}", AbstractClass),
("java", "interface Foo {}", Interface),
("java", "enum Foo { A }", Enum),
("java", "record Foo(int x) {}", Record),
("java", "@interface Foo {}", Annotation),
("java", "class Foo extends java.lang.RuntimeException {}", Exception),
("kotlin", "class Foo", Class),
("kotlin", "abstract class Foo", AbstractClass),
("kotlin", "interface Foo", Interface),
("kotlin", "enum class Foo { A }", Enum),
("kotlin", "annotation class Foo", Annotation),
("kotlin", "class Foo : RuntimeException()", Exception),
("kotlin", "object Foo {}", Object),
("kotlin", "data class Foo(val x: Int)", Class),
] {
assert_eq!(presentation(language, source).kind, Some(kind), "{language}: {source}");
}
}
#[test]
fn boot_annotations_are_on_the_primary_type_only() {
for language in ["java", "kotlin"] {
for annotation in ["@SpringBootApplication", "@org.springframework.boot.autoconfigure.SpringBootApplication"] {
assert!(presentation(language, &format!("{annotation} class Foo {{}}")).spring_boot);
}
for source in [
"// @SpringBootApplication\nclass Foo {}",
"/* @SpringBootApplication */ class Foo {}",
"@Service class Foo {}",
"@Controller class Foo {}",
"@Wrapper(\"@SpringBootApplication\") class Foo {}",
"class Outer { @SpringBootApplication class Nested {} } class Foo {}",
] {
assert!(!presentation(language, source).spring_boot, "{source}");
}
}
assert!(!presentation("java", "class Foo { String s = \"@SpringBootApplication\"; }").spring_boot);
assert!(!presentation("kotlin", "class Foo { val s = \"@SpringBootApplication\" }").spring_boot);
assert!(!presentation("java", "class Foo { @SpringBootApplication void method() {} }").spring_boot);
assert!(!presentation("java", "@Wrapper(@SpringBootApplication) class Foo {}").spring_boot);
assert_eq!(presentation("java", "class Foo extends Box<RuntimeException> {}").kind, Some(Class));
assert_eq!(presentation("kotlin", "class Foo : Box<RuntimeException>()").kind, Some(Class));
assert_eq!(presentation("java", "// no type\n"), JvmPresentation::default());
assert_eq!(presentation("kotlin", "val text = \"class Foo\""), JvmPresentation::default());
assert_eq!(presentation("java", "interface Other {} class Foo {} ").kind, Some(Class));
}
#[test]
fn runnable_methods() {
assert!(presentation("java", "class Foo { public static void main(String[] args) {} }").runnable);
assert!(presentation("java", "class Foo { public static void main(String... args) {} }").runnable);
assert!(!presentation("java", "class Foo { void main() {} }").runnable);
assert!(!presentation("java", "class Foo { class Nested { public static void main(String[] a) {} } }").runnable);
assert!(!presentation("java", "class Foo { String s = \"public static void main(String[] args) {}\"; }").runnable);
assert!(
presentation(
"kotlin",
"class Foo {
companion object {
@JvmStatic
fun main(args: Array<String>) {}
}
}"
)
.runnable
);
}
+1
View File
@@ -1,6 +1,7 @@
//! Test helpers: sources are plain strings; positions are named by the n-th
//! whole-word occurrence of an identifier (1-based), so fixtures stay readable.
mod jvm;
mod locals_langs;
mod structure;
+3
View File
@@ -0,0 +1,3 @@
# Per-user IDE state
/shelf/
/changelists.json
+1
View File
@@ -151,6 +151,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
doc::doc_folding,
doc::doc_highlights,
doc::doc_selection_ranges,
syntax::jvm_file_presentations,
syntax::syntax_support,
syntax::syntax_node_at,
syntax::syntax_expression_at,
+10
View File
@@ -112,3 +112,13 @@ pub async fn syntax_structural_ranges(
) -> IdeResult<StructuralRanges> {
ide_host::ops::syntax::syntax_structural_ranges(&state.host, window.label(), doc, pos, request).await
}
#[tauri::command]
#[specta::specta]
pub async fn jvm_file_presentations(
window: Window,
state: State<'_, AppState>,
paths: Vec<PathBuf>,
) -> IdeResult<Vec<ide_host::ops::jvm_icons::JvmFilePresentation>> {
ide_host::ops::jvm_icons::jvm_file_presentations(&state.host, window.label(), paths).await
}
+14
View File
@@ -226,6 +226,7 @@ export const commands = {
* Large documents fall back to word / line / document ranges.
*/
docSelectionRanges: (doc: DocId, offset: number) => typedError<Range[], IdeError>(__TAURI_INVOKE("doc_selection_ranges", { doc, offset })),
jvmFilePresentations: (paths: string[]) => typedError<JvmFilePresentation[], IdeError>(__TAURI_INVOKE("jvm_file_presentations", { paths })),
/**
* What the structure service supports for a language (`tree`: every query,
* `locals`: scope analysis).
@@ -3737,6 +3738,19 @@ export type JdkVendor = {
name: string,
};
export type JvmFilePresentation = {
path: string,
presentation: JvmPresentation,
};
export type JvmPresentation = {
kind: JvmTypeKind | null,
springBoot: boolean,
runnable: boolean,
};
export type JvmTypeKind = "class" | "abstractClass" | "interface" | "enum" | "record" | "annotation" | "exception" | "object";
/** Editor › Code Style › <Language> › Tabs and Indents (unset fields inherit). */
export type LanguageCodeStyle = LanguageCodeStyle_Serialize | LanguageCodeStyle_Deserialize;
+10 -1
View File
@@ -1,3 +1,4 @@
import { useFilePresentationIcons } from "@/platform/filePresentation";
/**
* The editor area: split layout of panes, each with IDEA-style tabs (preview
* tab in italics, pinned tabs, modified dot, file icons coloured by file
@@ -145,6 +146,9 @@ function PaneView({ paneId }: { paneId: string }) {
const { showExtension, markModified, showCloseButton, showFileIcon, showDirectoryForNonUnique, showFullPathOnHover } =
tabPrefs;
const fileIcons = useFilePresentationIcons(
showFileIcon ? tabs.flatMap((t) => (t.input.kind === "file" && t.input.path ? [t.input.path] : [])) : [],
);
const items: TabItem[] = useMemo(() => {
const names = tabs.map((t) => tabTitle(t.input, kinds));
return tabs.map((t, i) => {
@@ -167,7 +171,11 @@ function PaneView({ paneId }: { paneId: string }) {
</>
),
text: dupe ? `${name} — ${dupe}` : name,
icon: showFileIcon ? tabIcon(t.input, kinds) : undefined,
icon: showFileIcon
? t.input.kind === "file" && t.input.path
? fileIcons.get(t.input.path)
: tabIcon(t.input, kinds)
: undefined,
italic: t.preview,
modified: dirty[i],
closable: showCloseButton,
@@ -178,6 +186,7 @@ function PaneView({ paneId }: { paneId: string }) {
};
});
}, [
fileIcons,
tabs,
kinds,
dirty,
@@ -125,3 +125,16 @@
padding: 8px 12px;
color: var(--fg-subtle);
}
.iconStack {
position: relative;
display: inline-flex;
flex: none;
width: 16px;
height: 16px;
}
.runnable {
position: absolute;
inset: 0;
pointer-events: none;
}
+36 -10
View File
@@ -24,7 +24,7 @@ import { setContext } from "@/platform/context";
import { tx, useTxTemplate } from "@/platform/i18n";
import { isTauri } from "@/platform/ipc";
import type { RootFolder } from "@/platform/languageSettings";
import { useEffectiveRoots } from "@/platform/projectRoots";
import { useDetectedRoots, useEffectiveRoots } from "@/platform/projectRoots";
import { macStroke, strokeOf } from "@/platform/keymap";
import { OWNS_FILE_DROP, prepareMenuGroups, useFileDecoration } from "@/workbench/contributions";
import {
@@ -44,6 +44,9 @@ import {
} from "@/ui";
import { contextMenuItems, menuGroupContext, newMenuItems } from "./contextMenu";
import { openNewPopup } from "./newPopupModel";
import { presentationIcon, useJvmPresentation } from "@/platform/filePresentation";
import { moduleFolderIcon, useProjectPresentation, type BuildPresentation } from "@/platform/projectPresentation";
import { relativePath } from "@/platform/project";
import { folderKind } from "./folderKind";
import { useHomeDir } from "./home";
import { keyLike, nodeKeyFor, visibleInfo, type ProjectNode } from "./model";
@@ -109,11 +112,23 @@ const getLabel = (n: ProjectNode) => n.label;
const isExpandable = (n: ProjectNode) => n.dir && !n.placeholder;
const loadChildren = (n: ProjectNode) => loadDir(n.path);
function iconFor(node: ProjectNode, roots: readonly RootFolder[], root: string | null): string {
function iconFor(
node: ProjectNode,
roots: readonly RootFolder[],
root: string | null,
build: BuildPresentation,
detected: readonly RootFolder[],
): string {
if (node.placeholder) return node.placeholder === "dir" ? "folder" : "file";
if (node.icon) return node.icon;
if (node.root) return "module";
if (node.dir) return folderIconName(folderKind(node, roots, root));
if (node.dir) {
const kind = folderKind(node, roots, root, build, detected);
if (kind !== "plain") return folderIconName(kind);
const rel = root ? (node.path === root ? "" : relativePath(root, node.path)) : null;
const module = rel === null ? null : moduleFolderIcon(rel, node.root, build.modules);
if (module) return module;
return node.root ? "module" : "folder";
}
return fileIconName(node.label);
}
@@ -246,7 +261,13 @@ const Row = memo(function Row({
const deco = useFileDecoration(node.path);
const roots = useEffectiveRoots();
const root = useProjectTree((s) => s.root);
const icon = iconFor(node, roots, root);
const detected = useDetectedRoots();
const build = useProjectPresentation();
const file = useJvmPresentation(node.dir || node.placeholder ? "" : node.path, node.entry?.mtimeMs);
const icon =
node.dir || node.placeholder || node.icon
? iconFor(node, roots, root, build, detected)
: presentationIcon(node.path, file, roots, root, build.spring);
const isEditing =
!!editing && (node.placeholder ? editing.kind === "new" : editing.kind === "rename" && editing.key === node.key);
const faded = node.ignored || deco.faded;
@@ -263,11 +284,16 @@ const Row = memo(function Row({
onDoubleClick={(e) => onDoubleClick(node, e)}
onPointerDown={(e) => onPointerDown(node, e)}
>
<Icon
name={icon}
size={16}
className={clsx(styles.icon, node.dir && styles.folderIcon, dimIcon && styles.faded)}
/>
<span className={styles.iconStack}>
<Icon
name={icon}
size={16}
className={clsx(styles.icon, node.dir && styles.folderIcon, dimIcon && styles.faded)}
/>
{!!file?.runnable && icon !== "spring-boot" && icon !== "jvm-test" && (
<Icon name="runnable-mark" size={16} className={styles.runnable} />
)}
</span>
{isEditing && editing ? (
<InlineEditor
editing={editing}
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { folderKind, outputFolderKind } from "./folderKind";
import type { RootFolder } from "@/platform/languageSettings";
const build = {
modules: new Map([
["", "maven"],
["app", "gradle"],
] as const),
spring: false,
};
const roots: RootFolder[] = [
{ path: "src/main/java", kind: "source", detectedFrom: "pom.xml" },
{ path: "src/test/java", kind: "test", detectedFrom: "pom.xml" },
{ path: "src/main/resources", kind: "resources", detectedFrom: "pom.xml" },
{ path: "src/test/resources", kind: "testResources", detectedFrom: "pom.xml" },
];
const kind = (rel: string, excluded = false, rs = roots) =>
folderKind({ path: `/p/${rel}`, excluded }, rs, "/p", build);
describe("JVM folder presentation", () => {
it.each([
["src/main/java", "source"],
["src/test/java", "test"],
["src/main/resources", "resources"],
["src/test/resources", "testResources"],
["src/main/java/com/example", "package"],
["src/test/java/com", "package"],
["src/main/resources/templates", "plain"],
["plain", "plain"],
])("%s: %s", (path, expected) => expect(kind(path)).toBe(expected));
it.each([
["target/generated-sources", "generatedSource"],
["target/generated-test-sources", "generatedTest"],
["app/build/generated", "generatedSource"],
["app/build/classes/kotlin", "compiled"],
["target/classes", "compiled"],
["target/site/apidocs", "javadoc"],
["app/build/docs/javadoc", "javadoc"],
])("%s: %s", (path, expected) => {
expect(kind(path)).toBe(expected);
expect(kind(path, true)).toBe("excluded");
expect(outputFolderKind(path, new Map())).toBeNull();
});
it("shows packages below custom marked JVM source roots", () => {
expect(kind("custom/com/example", false, [{ path: "custom", kind: "source", detectedFrom: null }])).toBe("package");
});
it("keeps marks and generated source roots ahead of output and exclusion", () => {
expect(
kind("target/generated-sources", true, [
{ path: "target/generated-sources", kind: "source", detectedFrom: "pom.xml" },
]),
).toBe("source");
expect(kind("target/classes", false, [{ path: "target/classes", kind: "test", detectedFrom: null }])).toBe("test");
expect(
kind("src/main/java/com", false, [...roots, { path: "src/main/java/com", kind: "excluded", detectedFrom: null }]),
).toBe("excluded");
});
it.each(["generated-sources", "src/main/java/classes", "other/build/generated", "build/generated"])(
"does not guess output roles for %s",
(path) => expect(outputFolderKind(path, build.modules)).toBeNull(),
);
});
+35 -3
View File
@@ -1,8 +1,10 @@
/** Folder icon kind in the project view, from the effective roots. */
import { rootRole, type FolderRole, type RootFolder } from "@/platform/languageSettings";
import { rootFor, rootRole, type FolderRole, type RootFolder } from "@/platform/languageSettings";
import { basename, relativePath } from "@/platform/project";
import type { FolderKind } from "@/ui";
import type { ProjectNode } from "./model";
import { packageKindAt } from "./packages";
import type { BuildPresentation } from "@/platform/projectPresentation";
const ROLE_KIND: Record<FolderRole, FolderKind> = {
source: "source",
@@ -20,9 +22,39 @@ export function folderKind(
node: Pick<ProjectNode, "path" | "excluded">,
roots: readonly RootFolder[],
root: string | null,
build?: BuildPresentation,
detected: readonly RootFolder[] = [],
): FolderKind {
if (node.excluded) return "excluded";
const role = root ? rootRole(roots, relativePath(root, node.path)) : null;
const rel = root ? (node.path === root ? "" : relativePath(root, node.path)) : null;
const role = rel === null ? null : rootRole(roots, rel);
const exact = roots.find((r) => r.path === rel);
const output = rel === null ? null : outputFolderKind(rel, build?.modules ?? new Map());
if (role && (exact?.detectedFrom === null || output === "generatedSource" || output === "generatedTest"))
return ROLE_KIND[role];
const containing = rel === null ? null : rootFor(roots, rel);
if (node.excluded || containing?.kind === "excluded") return "excluded";
if (role) return ROLE_KIND[role];
if (output) return output;
if (rel !== null && (containing?.kind === "source" || containing?.kind === "test")) {
const inModule = [...(build?.modules.keys() ?? [])].some((dir) => !dir || rel.startsWith(`${dir}/`));
if (inModule || packageKindAt(rel, roots, detected) === "jvm") return "package";
}
return basename(node.path) === ".github" ? "github" : "plain";
}
/** Output roles are relative to detected JVM modules, never arbitrary package names. */
export function outputFolderKind(rel: string, modules: BuildPresentation["modules"]): FolderKind | null {
const parts = rel.split("/");
for (let i = 0; i < parts.length - 1; i++) {
const output = parts[i];
if (output !== "target" && output !== "build") continue;
const tool = modules.get(parts.slice(0, i).join("/"));
if (tool !== (output === "target" ? "maven" : "gradle")) continue;
const path = parts.slice(i + 1).join("/");
if (/^generated-test-sources(\/|$)/.test(path) || /^generated\/.*\btest\b/.test(path)) return "generatedTest";
if (/^(generated-sources|generated)(\/|$)/.test(path)) return "generatedSource";
if (/^(classes|test-classes)(\/|$)/.test(path)) return "compiled";
if (/^(site\/apidocs|apidocs|docs\/javadoc|javadoc)(\/|$)/.test(path)) return "javadoc";
}
return null;
}
@@ -1,3 +1,4 @@
import { useFilePresentation } from "@/platform/filePresentation";
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type MouseEvent } from "react";
import clsx from "clsx";
import { actionToggled, executeAction, getAction } from "@/platform/actions";
@@ -16,7 +17,6 @@ import {
SymbolBadge,
Select,
Toggle,
fileIconName,
listNavigate,
type MenuItem,
T,
@@ -830,9 +830,10 @@ function FileRow({
line: number | null;
}) {
const deco = useFileDecoration(path);
const { icon } = useFilePresentation(path);
return (
<>
<Icon name={fileIconName(name)} size={16} className={styles.icon} />
<Icon name={icon} size={16} className={styles.icon} />
<span className={styles.main}>
<span className={styles.name} style={deco.color ? { color: deco.color } : undefined}>
<HighlightedText text={name} indices={indices} tone="strong" />
+3 -1
View File
@@ -1,3 +1,4 @@
import { useFilePresentation } from "@/platform/filePresentation";
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type MouseEvent as ReactMouseEvent } from "react";
import clsx from "clsx";
import { editorsFor, getNavigationHistory } from "@/editor/api";
@@ -353,9 +354,10 @@ function ToolRow({ tw, indices }: { tw: ToolWindowContribution; indices: number[
function RecentFileRow({ item, current }: { item: RecentFileItem; current: boolean }) {
const deco = useFileDecoration(item.editor ? "" : item.path);
const { icon } = useFilePresentation(item.editor ? "" : item.path);
return (
<>
<Icon name={item.editor?.icon ?? fileIconName(item.name)} size={16} className={styles.icon} />
<Icon name={item.editor?.icon ?? icon} size={16} className={styles.icon} />
<span
className={clsx(styles.name, current && styles.current)}
style={deco.color ? { color: deco.color } : undefined}
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, renderHook } from "@testing-library/react";
const backend = vi.hoisted(() => ({
resolve: vi.fn(),
event: undefined as undefined | ((e: { created: string[]; modified: string[]; removed: string[] }) => void),
}));
vi.mock("./ipc", async (original) => ({
...(await original<typeof import("./ipc")>()),
isTauri: () => true,
commands: { jvmFilePresentations: backend.resolve },
onIdeEvent: (name: string, handler: typeof backend.event) => {
if (name === "fsChanged") backend.event = handler;
return () => {};
},
}));
vi.mock("./projectPresentation", () => ({ useProjectPresentation: () => ({ spring: false, modules: new Map() }) }));
import { useFilePresentation, useFilePresentationIcons } from "./filePresentation";
const answer = (path: string, kind = "interface") => ({
path,
presentation: { kind, springBoot: false, runnable: false },
});
const tick = async () => {
await act(async () => {
await vi.runOnlyPendingTimersAsync();
});
};
describe("visible file resolver", () => {
beforeEach(() => {
vi.useFakeTimers();
backend.resolve
.mockReset()
.mockImplementation(async (paths: string[]) => ({ status: "ok", data: paths.map((p) => answer(p)) }));
});
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it("shows a plain icon immediately and batches mounted paths only", async () => {
const { result } = renderHook(() => useFilePresentationIcons(["/p/Foo.java", "/p/Bar.kt", "/p/config.yml"]));
expect(result.current.get("/p/Foo.java")).toBe("file-java");
expect(backend.resolve).not.toHaveBeenCalled();
await tick();
expect(backend.resolve).toHaveBeenCalledOnce();
expect(backend.resolve.mock.calls[0]?.[0]).toEqual(["/p/Foo.java", "/p/Bar.kt"]);
expect(result.current.get("/p/Foo.java")).toBe("interface");
});
it("drops unmounted requests and splits large sets into bounded batches", async () => {
const dropped = renderHook(() => useFilePresentation("/p/Invisible.java"));
dropped.unmount();
const { result } = renderHook(() =>
useFilePresentationIcons(Array.from({ length: 130 }, (_, i) => `/p/F${i}.java`)),
);
await tick();
await tick();
await tick();
expect(backend.resolve.mock.calls.map(([paths]) => paths.length)).toEqual([64, 64, 2]);
expect(result.current.get("/p/F129.java")).toBe("interface");
expect(backend.resolve.mock.calls.flatMap(([paths]) => paths)).not.toContain("/p/Invisible.java");
});
it("invalidates on watcher changes and rejects an in-flight stale result", async () => {
let finish!: (value: unknown) => void;
backend.resolve.mockImplementationOnce(
() =>
new Promise((resolve) => {
finish = resolve;
}),
);
const { result } = renderHook(() => useFilePresentation("/p/Foo.java"));
await tick();
act(() => backend.event?.({ created: [], modified: ["/p/Foo.java"], removed: [] }));
await act(async () => {
finish({ status: "ok", data: [answer("/p/Foo.java", "class")] });
});
expect(result.current.icon).toBe("file-java");
await tick();
expect(result.current.icon).toBe("interface");
expect(backend.resolve).toHaveBeenCalledTimes(2);
});
it("rechecks changed mtimes while preserving cached results for unchanged rows", async () => {
const { result, rerender } = renderHook(({ mtime }) => useFilePresentation("/p/Foo.java", mtime), {
initialProps: { mtime: 1 },
});
await tick();
rerender({ mtime: 1 });
await tick();
expect(backend.resolve).toHaveBeenCalledOnce();
rerender({ mtime: 2 });
await tick();
expect(backend.resolve).toHaveBeenCalledTimes(2);
expect(result.current.icon).toBe("interface");
});
it("caches failures instead of continuously retrying visible batches", async () => {
backend.resolve.mockRejectedValue(new Error("offline"));
renderHook(() => useFilePresentationIcons(Array.from({ length: 65 }, (_, i) => `/p/F${i}.java`)));
await tick();
await tick();
await tick();
expect(backend.resolve).toHaveBeenCalledTimes(2);
});
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { presentationIcon } from "./filePresentation";
import type { RootFolder } from "./languageSettings";
import type { JvmPresentation } from "./ipc";
const roots: RootFolder[] = [
{ path: "src/main/java", kind: "source", detectedFrom: "pom.xml" },
{ path: "src/test/java", kind: "test", detectedFrom: "pom.xml" },
{ path: "src/main/resources", kind: "resources", detectedFrom: "pom.xml" },
{ path: "src/test/resources", kind: "testResources", detectedFrom: "pom.xml" },
];
const type = (kind: JvmPresentation["kind"]): JvmPresentation => ({ kind, springBoot: false, runnable: false });
const icon = (path: string, info: JvmPresentation | null = null, spring = false) =>
presentationIcon(`/p/${path}`, info, roots, "/p", spring);
describe("JVM file presentation", () => {
it.each([
["class", "class"],
["abstractClass", "class-abstract"],
["interface", "interface"],
["enum", "enum"],
["record", "record"],
["annotation", "annotation"],
["exception", "exception"],
["object", "kotlin-object"],
] as const)("maps %s to %s", (kind, expected) => expect(icon("Foo.java", type(kind))).toBe(expected));
it.each(["LIMIT.java", "AUDIT.java", "Contest.java"])("keeps the type icon of %s", (path) =>
expect(icon(path, type("class"))).toBe("class"),
);
it.each(["FooTest.java", "FooTests.java", "FooIT.java", "FooTest.kt", "src/test/java/Anything.java"])(
"marks test classes: %s",
(path) => expect(icon(path, type("class"))).toBe("jvm-test"),
);
it("keeps files without declarations and binaries as file types", () => {
expect(icon("Foo.java", type(null))).toBe("file-java");
expect(icon("FooTest.java", type(null))).toBe("file-java");
expect(icon("Foo.kt")).toBe("file-kotlin");
expect(icon("Foo.class")).toBe("file-binary");
expect(icon("Contest.java", type("class"))).toBe("class");
});
it.each(["application.yml", "application-dev.yaml", "bootstrap.properties", "bootstrap-local.yml"])(
"requires Spring or a resource root: %s",
(name) => {
expect(icon(`src/main/resources/${name}`)).toBe("spring-config");
expect(icon(`src/test/resources/${name}`)).toBe("spring-config");
expect(icon(name, null, true)).toBe("spring-config");
expect(icon(name)).not.toBe("spring-config");
},
);
it("uses the entire Boot icon on application classes", () => {
expect(icon("App.java", { ...type("class"), springBoot: true, runnable: true })).toBe("spring-boot");
});
});
+189
View File
@@ -0,0 +1,189 @@
/** Visible-file JVM presentation: one bounded IPC batch, shared cache and watcher. */
import { useEffect, useMemo } from "react";
import { create } from "zustand";
import { fileIconName, type IconName } from "@/ui/icons";
import { call, commands, isTauri, onIdeEvent, type JvmPresentation } from "./ipc";
import { basename, relativePath, useProject } from "./project";
import { rootFor, type RootFolder } from "./languageSettings";
import { useEffectiveRoots } from "./projectRoots";
import { useProjectPresentation } from "./projectPresentation";
const JVM = /\.(java|kt)$/;
const MAX_CACHE = 4096;
const pending = new Map<string, number>();
const visible = new Map<string, number>();
const versions = new Map<string, number>();
const mtimes = new Map<string, number>();
const cache = create<{ entries: ReadonlyMap<string, JvmPresentation | null> }>(() => ({ entries: new Map() }));
let timer: ReturnType<typeof setTimeout> | undefined;
let running = false;
let epoch = 0;
let dispose: (() => void) | undefined;
let users = 0;
function enqueue(path: string) {
if (!JVM.test(path) || cache.getState().entries.has(path) || !isTauri()) return;
pending.set(path, versions.get(path) ?? 0);
if (!running && !timer)
timer = setTimeout(() => {
timer = undefined;
void flush();
}, 0);
}
async function flush() {
if (running) return;
const batch = [...pending].filter(([p]) => visible.has(p)).slice(0, 64);
pending.clear();
if (!batch.length) return;
running = true;
const gen = epoch;
try {
const results = await call(commands.jvmFilePresentations(batch.map(([p]) => p)));
if (gen !== epoch) return;
const entries = new Map(cache.getState().entries);
for (const [path, version] of batch) {
if ((versions.get(path) ?? 0) !== version) continue;
entries.delete(path);
entries.set(path, results.find((r) => r.path === path)?.presentation ?? null);
}
while (entries.size > MAX_CACHE) entries.delete(entries.keys().next().value!);
cache.setState({ entries });
} catch {
if (gen === epoch) {
const entries = new Map(cache.getState().entries);
for (const [path, version] of batch) if ((versions.get(path) ?? 0) === version) entries.set(path, null);
cache.setState({ entries });
}
} finally {
running = false;
for (const path of versions.keys()) if (!visible.has(path)) versions.delete(path);
// Requests that arrived during IPC stay bounded to the mounted consumers.
for (const p of visible.keys()) if (!batch.some(([b]) => b === p)) enqueue(p);
if (pending.size && !timer)
timer = setTimeout(() => {
timer = undefined;
void flush();
}, 0);
}
}
export function invalidateFilePresentations(paths: readonly string[]) {
const entries = new Map(cache.getState().entries);
for (const key of new Set([...entries.keys(), ...visible.keys()])) {
if (!paths.some((p) => key === p || key.startsWith(p + "/"))) continue;
entries.delete(key);
versions.set(key, (versions.get(key) ?? 0) + 1);
}
cache.setState({ entries });
for (const p of visible.keys()) enqueue(p);
}
function mount(path: string) {
if (!JVM.test(path)) return () => {};
if (users++ === 0) {
cache.setState({ entries: new Map() });
mtimes.clear();
const fs = onIdeEvent("fsChanged", (e) => invalidateFilePresentations([...e.created, ...e.modified, ...e.removed]));
const project = useProject.subscribe((s, prev) => {
if (s.project === prev.project) return;
epoch++;
versions.clear();
pending.clear();
cache.setState({ entries: new Map() });
});
dispose = () => {
fs();
project();
};
}
visible.set(path, (visible.get(path) ?? 0) + 1);
enqueue(path);
return () => {
const count = (visible.get(path) ?? 1) - 1;
if (count) visible.set(path, count);
else {
visible.delete(path);
pending.delete(path);
}
if (--users === 0) {
epoch++;
pending.clear();
dispose?.();
}
};
}
export function presentationIcon(
path: string,
info: JvmPresentation | null | undefined,
roots: readonly RootFolder[],
root: string | null,
spring: boolean,
): IconName {
const name = basename(path);
const role = root ? rootFor(roots, relativePath(root, path))?.kind : null;
if (
/^(application|bootstrap).*\.(yml|yaml|properties)$/.test(name) &&
(spring || role === "resources" || role === "testResources")
)
return "spring-config";
if (!info?.kind) return fileIconName(name);
if (info.springBoot) return "spring-boot";
if (/(Tests?|[a-z\d]IT)\.(java|kt)$/.test(name) || role === "test") return "jvm-test";
const kind: Record<NonNullable<JvmPresentation["kind"]>, IconName> = {
class: "class",
abstractClass: "class-abstract",
interface: "interface",
enum: "enum",
record: "record",
annotation: "annotation",
exception: "exception",
object: "kotlin-object",
};
return kind[info.kind];
}
export function useJvmPresentation(path: string, mtime?: number | null) {
useEffect(() => (path ? mount(path) : undefined), [path]);
// Directory refresh metadata also invalidates results if a watcher event was missed.
useEffect(() => {
if (mtime != null && JVM.test(path)) {
const previous = mtimes.get(path);
mtimes.set(path, mtime);
if (previous !== undefined && previous !== mtime) invalidateFilePresentations([path]);
if (mtimes.size > MAX_CACHE) mtimes.delete(mtimes.keys().next().value!);
}
}, [path, mtime]);
return cache((s) => s.entries.get(path));
}
export function useFilePresentation(path: string, mtime?: number | null) {
const info = useJvmPresentation(path, mtime);
const roots = useEffectiveRoots();
const root = useProject((s) => s.project?.root ?? null);
const build = useProjectPresentation();
const icon = presentationIcon(path, info, roots, root, build.spring);
return { icon, runnable: !!info?.runnable && icon !== "spring-boot" && icon !== "jvm-test" };
}
/** Tab strips resolve their mounted file tabs through the same batch as tree rows. */
export function useFilePresentationIcons(paths: readonly string[]) {
const key = JSON.stringify(paths);
useEffect(() => {
const disposers = (JSON.parse(key) as string[]).map(mount);
return () => disposers.forEach((dispose) => dispose());
}, [key]);
const entries = cache((s) => s.entries);
const roots = useEffectiveRoots();
const root = useProject((s) => s.project?.root ?? null);
const build = useProjectPresentation();
return useMemo(
() =>
new Map(
(JSON.parse(key) as string[]).map((path) => [
path,
presentationIcon(path, entries.get(path), roots, root, build.spring),
]),
),
[key, entries, roots, root, build.spring],
);
}
+63
View File
@@ -0,0 +1,63 @@
import { describe, it, expect } from "vitest";
import { buildPresentation, moduleFolderIcon } from "./projectPresentation";
import type { BuildModel } from "./ipc";
const model = (partial: unknown) => ({ maven: null, gradle: null, ...(partial as object) }) as BuildModel;
describe("build presentation context", () => {
it.each(["maven", "gradle"] as const)("uses distinct %s root and nested module glyphs", (tool) => {
const modules = new Map([
["", tool],
["app", tool],
]);
expect(moduleFolderIcon("", true, modules)).toBe(`${tool}-project`);
expect(moduleFolderIcon("app", false, modules)).toBe(`${tool}-module`);
expect(moduleFolderIcon("plain", false, modules)).toBeNull();
});
it("includes Maven aggregators and modules without source roots", () => {
const context = buildPresentation(
model({
maven: {
projects: [
{ dir: "", dependencies: [], plugins: [] },
{ dir: "app", dependencies: [{ groupId: "org.springframework.boot" }], plugins: [] },
],
},
}),
);
expect([...context.modules]).toEqual([
["", "maven"],
["app", "maven"],
]);
expect(context.spring).toBe(true);
});
it("includes Gradle root and nested project directories", () => {
const context = buildPresentation(
model({
gradle: {
projects: [
{ dir: "", tasks: [] },
{ dir: "lib/core", tasks: [] },
],
},
}),
);
expect([...context.modules]).toEqual([
["", "gradle"],
["lib/core", "gradle"],
]);
expect(context.spring).toBe(false);
});
it("detects Spring build plugins and Gradle Boot tasks", () => {
expect(
buildPresentation(
model({
maven: { projects: [{ dir: "", dependencies: [], plugins: [{ groupId: "org.springframework.boot" }] }] },
}),
).spring,
).toBe(true);
expect(buildPresentation(model({ gradle: { projects: [{ dir: "", tasks: [{ name: "bootRun" }] }] } })).spring).toBe(
true,
);
});
it("keeps plain folders plain", () => expect(buildPresentation(model({})).modules.size).toBe(0));
});
+83
View File
@@ -0,0 +1,83 @@
/** Read-only build context shared by file and folder presentation consumers. */
import { useEffect } from "react";
import { create } from "zustand";
import { call, commands, isTauri, onIdeEvent, type BuildModel } from "./ipc";
import { useProject } from "./project";
export interface BuildPresentation {
modules: ReadonlyMap<string, "maven" | "gradle">;
spring: boolean;
}
const EMPTY: BuildPresentation = { modules: new Map(), spring: false };
export const useBuildPresentation = create<BuildPresentation>(() => EMPTY);
export function buildPresentation(model: BuildModel): BuildPresentation {
const modules = new Map<string, "maven" | "gradle">();
for (const p of model.maven?.projects ?? []) modules.set(p.dir, "maven");
for (const p of model.gradle?.projects ?? []) modules.set(p.dir, "gradle");
const spring =
(model.maven?.projects ?? []).some(
(p) =>
p.dependencies.some((d) => d.groupId.startsWith("org.springframework")) ||
p.plugins.some((p) => p.groupId.startsWith("org.springframework")),
) || (model.gradle?.projects ?? []).some((p) => p.tasks.some((t) => t.name === "bootRun"));
return { modules, spring };
}
export function moduleFolderIcon(rel: string, root: boolean, modules: BuildPresentation["modules"]): string | null {
const tool = modules.get(rel);
return tool ? `${tool}-${root ? "project" : "module"}` : null;
}
let users = 0;
let generation = 0;
let dispose: (() => void) | undefined;
async function load() {
const gen = ++generation;
if (!useProject.getState().project || !isTauri()) {
useBuildPresentation.setState(EMPTY);
return;
}
try {
const model = await call(commands.buildModel());
if (generation === gen) useBuildPresentation.setState(buildPresentation(model));
} catch {
/* Presentation keeps its filename fallback. */
}
}
export function useProjectPresentation(): BuildPresentation {
useEffect(() => {
if (users++ === 0) {
void load();
const project = useProject.subscribe((s, prev) => {
if (s.project === prev.project) return;
useBuildPresentation.setState(EMPTY);
void load();
});
const roots = onIdeEvent("projectRootsChanged", () => void load());
let timer: ReturnType<typeof setTimeout> | undefined;
const files = onIdeEvent("fsChanged", (e) => {
if (
![...e.created, ...e.modified, ...e.removed].some((p) =>
/(?:^|\/)(pom\.xml|(?:build|settings)\.gradle(?:\.kts)?)$/.test(p),
)
)
return;
clearTimeout(timer);
timer = setTimeout(() => void load(), 100);
});
dispose = () => {
project();
roots();
files();
clearTimeout(timer);
generation++;
};
}
return () => {
if (--users === 0) dispose?.();
};
}, []);
return useBuildPresentation();
}
+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8944 3.54486C14.5319 2.78856 13.3381 1.8948 12.0006 3.06357M14.8944 3.54486C15.3569 4.42607 14.5631 6.06982 12.9631 6.06982C10.8767 6.06982 8.68787 2.61277 3.9237 4.96165C3.50279 5.16918 3.31562 5.6629 3.45956 6.10957L3.57564 6.46982M14.8944 3.54486C14.8944 3.54486 16.2295 6.36001 12.5643 9.10175C11.5314 9.87523 11.153 11.3935 11.1147 12.7459C11.1132 12.8004 11.0688 12.8441 11.0142 12.8441H9.87321C9.77409 12.8441 9.69136 12.771 9.66556 12.6753C9.4931 12.0357 8.90943 11.5836 8.22997 11.5836C7.55227 11.5836 6.96987 12.0398 6.79573 12.6779C6.76971 12.7732 6.68713 12.8459 6.58832 12.8459H5.51173C5.41292 12.8459 5.33034 12.7732 5.30432 12.6779C5.13019 12.0398 4.54778 11.5836 3.87008 11.5836C3.19218 11.5836 2.60962 12.0401 2.43568 12.6784C2.40969 12.7738 2.32709 12.8466 2.22825 12.8466H1.03554C0.984575 12.8466 0.941657 12.8077 0.936422 12.757C0.642735 9.91312 1.46647 7.80583 3.57564 6.46982M3.57564 6.46982L4.15687 8.38056C4.37284 9.01618 5.24066 9.12128 5.77023 8.82391C6.42801 8.46822 7.03442 8.04701 7.59165 7.54507" stroke="#6C707E" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25933 7.32028C9.16406 7.25907 9.1068 7.14753 9.14402 7.04058C9.24352 6.75468 9.50016 6.54346 9.8117 6.5096C10.1856 6.46897 10.5372 6.69485 10.6556 7.05184C10.7543 7.34927 10.6688 7.67048 10.4501 7.8798C10.3683 7.9581 10.2431 7.95237 10.1478 7.89115L9.25933 7.32028Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.9016 4.65326C18.4322 3.67376 16.8861 2.51625 15.1539 4.02994M18.9016 4.65326C19.5006 5.79453 18.4726 7.92337 16.4004 7.92337C13.6672 7.92337 10.7983 3.3425 4.47909 6.59615C4.06186 6.81098 3.87557 7.30249 4.01951 7.74917L4.24258 8.44141M18.9016 4.65326C18.9016 4.65326 20.6308 8.2992 15.8839 11.8501C14.5388 12.8574 14.0507 14.8399 14.0058 16.5987C14.0044 16.6533 13.9599 16.6969 13.9054 16.6969H12.3477C12.2486 16.6969 12.1656 16.6239 12.1423 16.5276C11.9362 15.6732 11.1678 15.0644 10.2705 15.0644C9.37546 15.0644 8.60879 15.6787 8.40024 16.5307C8.37674 16.6267 8.29385 16.6992 8.19504 16.6992H6.69933C6.60052 16.6992 6.51763 16.6267 6.49413 16.5307C6.28558 15.6787 5.51891 15.0644 4.62391 15.0644C3.72864 15.0644 2.96177 15.6791 2.75349 16.5315C2.73003 16.6275 2.64712 16.7 2.54828 16.7H0.926436C0.875473 16.7 0.832559 16.6611 0.827236 16.6104C0.43905 12.9139 1.50445 10.1758 4.24258 8.44141M4.24258 8.44141L4.99534 10.916C5.27503 11.7392 6.39897 11.8754 7.08481 11.4902C7.93671 11.0296 8.72209 10.484 9.44376 9.83399" stroke="#6C707E" stroke-width="1.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5569 9.51317C11.4617 9.45196 11.4054 9.34109 11.4369 9.23233C11.5518 8.83589 11.8961 8.53859 12.3182 8.49271C12.8024 8.44009 13.2577 8.73263 13.4111 9.19497C13.5448 9.59798 13.4175 10.0347 13.1046 10.3039C13.0188 10.3778 12.8946 10.3726 12.7993 10.3114L11.5569 9.51317Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.9016 4.65326C18.4322 3.67376 16.8861 2.51625 15.1539 4.02994M18.9016 4.65326C19.5006 5.79453 18.4726 7.92337 16.4004 7.92337C13.6672 7.92337 10.7983 3.3425 4.47909 6.59615C4.06186 6.81098 3.87557 7.30249 4.01951 7.74917L4.24258 8.44141M18.9016 4.65326C18.9016 4.65326 20.6308 8.2992 15.8839 11.8501C14.5388 12.8574 14.0507 14.8399 14.0058 16.5987C14.0044 16.6533 13.9599 16.6969 13.9054 16.6969H12.3477C12.2486 16.6969 12.1656 16.6239 12.1423 16.5276C11.9362 15.6732 11.1678 15.0644 10.2705 15.0644C9.37546 15.0644 8.60879 15.6787 8.40024 16.5307C8.37674 16.6267 8.29385 16.6992 8.19504 16.6992H6.69933C6.60052 16.6992 6.51763 16.6267 6.49413 16.5307C6.28558 15.6787 5.51891 15.0644 4.62391 15.0644C3.72864 15.0644 2.96177 15.6791 2.75349 16.5315C2.73003 16.6275 2.64712 16.7 2.54828 16.7H0.926436C0.875473 16.7 0.832559 16.6611 0.827236 16.6104C0.43905 12.9139 1.50445 10.1758 4.24258 8.44141M4.24258 8.44141L4.99534 10.916C5.27503 11.7392 6.39897 11.8754 7.08481 11.4902C7.93671 11.0296 8.72209 10.484 9.44376 9.83399" stroke="#CED0D6" stroke-width="1.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5569 9.51317C11.4617 9.45196 11.4054 9.34109 11.4369 9.23233C11.5518 8.83589 11.8961 8.53859 12.3182 8.49271C12.8024 8.44009 13.2577 8.73263 13.4111 9.19497C13.5448 9.59798 13.4175 10.0347 13.1046 10.3039C13.0188 10.3778 12.8946 10.3726 12.7993 10.3114L11.5569 9.51317Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+6
View File
@@ -0,0 +1,6 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.565 10.3794C15.3324 10.1419 15.0185 10.0059 14.6895 10.0002C14.3605 9.99443 14.0422 10.1193 13.8018 10.3485C13.7897 10.3602 13.7801 10.3743 13.7736 10.3899C13.767 10.4055 13.7636 10.4223 13.7636 10.4393C13.7636 10.4562 13.767 10.473 13.7736 10.4887C13.7801 10.5043 13.7897 10.5183 13.8018 10.53L13.9547 10.6898C13.9754 10.7107 14.0028 10.7234 14.0319 10.7254C14.0609 10.7274 14.0898 10.7186 14.113 10.7007C14.2597 10.5811 14.4459 10.524 14.6329 10.5414C14.8198 10.5587 14.9929 10.6492 15.116 10.7938C15.2391 10.9385 15.3027 11.1261 15.2934 11.3174C15.2841 11.5088 15.2027 11.6891 15.0662 11.8206C14.6477 12.2474 14.171 11.9997 13.5378 11.6708C12.647 11.2079 11.4465 10.5842 9.66257 11.4521C9.61107 11.4774 9.56524 11.5132 9.52801 11.5575C9.49077 11.6017 9.46292 11.6533 9.4462 11.7091C9.42949 11.7649 9.42428 11.8236 9.43092 11.8815C9.43755 11.9394 9.45587 11.9953 9.48474 12.0457L10.0085 12.9687C10.0373 13.0199 10.0761 13.0638 10.1218 13.0981C10.1325 13.1059 10.1437 13.1131 10.1551 13.1198C10.2177 13.1565 10.2883 13.1754 10.3598 13.1752L10.3613 13.1752C10.4297 13.1753 10.4972 13.1575 10.5574 13.123L10.5609 13.121C10.9107 12.9279 11.231 12.7004 11.5244 12.4307C11.6261 12.3372 11.7842 12.3439 11.8777 12.4455C11.9711 12.5471 11.9645 12.7053 11.8628 12.7987C11.539 13.0965 11.1863 13.3468 10.8042 13.5578C10.669 13.6348 10.5164 13.6754 10.3609 13.6752C10.1998 13.6756 10.0417 13.6328 9.90255 13.5513C9.76323 13.4698 9.64805 13.3526 9.56811 13.2121L9.09541 12.3796C8.27149 13.1021 7.7946 14.3497 8.08603 15.8992C8.0912 15.9274 8.10583 15.9529 8.12742 15.9713C8.149 15.9897 8.17619 15.9998 8.2043 16H8.76271C8.79347 16.0008 8.82338 15.9896 8.84635 15.9687C8.86931 15.9478 8.8836 15.9187 8.88631 15.8874C8.91254 15.6847 9.01007 15.4986 9.16073 15.3638C9.31138 15.229 9.5049 15.1547 9.70525 15.1547C9.9056 15.1547 10.0991 15.229 10.2498 15.3638C10.4004 15.4986 10.498 15.6847 10.5242 15.8874C10.5277 15.9172 10.5417 15.9446 10.5637 15.9645C10.5856 15.9844 10.614 15.9954 10.6433 15.9954H11.1893C11.2186 15.9952 11.2469 15.9841 11.2688 15.9643C11.2907 15.9444 11.3048 15.9171 11.3085 15.8874C11.3347 15.6847 11.4322 15.4986 11.5829 15.3638C11.7336 15.229 11.9271 15.1547 12.1274 15.1547C12.3278 15.1547 12.5213 15.229 12.6719 15.3638C12.8226 15.4986 12.9201 15.6847 12.9464 15.8874C12.95 15.9171 12.9641 15.9444 12.986 15.9643C13.008 15.9841 13.0362 15.9952 13.0655 15.9954H13.6079C13.6397 15.9954 13.6702 15.9827 13.6928 15.9599C13.7154 15.9372 13.7284 15.9062 13.7288 15.8738C13.7413 15.0933 13.9476 14.1966 14.5354 13.7474C16.5716 12.1927 16.0363 10.8604 15.565 10.3794ZM12.6279 12.7721L13.4922 13.2142C13.5665 13.158 13.6256 13.0853 13.6637 12.9993C13.7018 12.9132 13.7176 12.8186 13.7097 12.7245C13.7018 12.6304 13.6705 12.5399 13.6186 12.4617C13.5667 12.3835 13.4961 12.3201 13.4135 12.2776C13.3308 12.2352 13.2389 12.215 13.1464 12.2191C13.054 12.2232 12.9641 12.2513 12.8853 12.3009C12.8066 12.3505 12.7416 12.4198 12.6965 12.5023C12.6515 12.5848 12.6278 12.6777 12.6279 12.7721Z" fill="#4682FA"/>
<path d="M1 3.86667C1 2.83574 1.73127 2 2.63333 2H6.1217C6.39413 2 6.65477 2.11115 6.84336 2.30775L8.46667 4L13 4C14.1046 4 15 4.89543 15 6V9.00019H14.6975C14.1063 8.99248 13.5387 9.21829 13.1124 9.62462L13.1072 9.62955L13.1055 9.63119C12.9964 9.73724 12.9105 9.86373 12.8522 10.0024C12.8195 10.0802 12.7957 10.1612 12.7812 10.2439C12.5697 10.1751 12.3421 10.1162 12.0993 10.0759C11.2547 9.93571 10.2982 10.031 9.22485 10.5533L9.22277 10.5543C9.04568 10.6411 8.88945 10.7639 8.76346 10.9135C8.63751 11.0631 8.54439 11.2366 8.48877 11.4221C8.46921 11.4874 8.4544 11.5539 8.44437 11.621L8.4366 11.6278C7.76612 12.2157 7.29304 13.0363 7.0988 14H2.63333C1.73127 14 1 13.1643 1 12.1333V3.86667Z" fill="#EBECF0"/>
<path d="M6.1217 3L2.63333 3C2.40481 3 2 3.25841 2 3.86667V12.1333C2 12.7416 2.40481 13 2.63333 13H7.42186C7.27921 13.311 7.17018 13.6459 7.0988 14H2.63333C1.73127 14 1 13.1643 1 12.1333V3.86667C1 2.83574 1.73127 2 2.63333 2H6.1217C6.39413 2 6.65477 2.11115 6.84336 2.30775L8.46667 4L13 4C14.1046 4 15 4.89543 15 6V9.00019H14.6975C14.4586 8.99708 14.2235 9.03211 14 9.10179V6C14 5.44771 13.5523 5 13 5H8.04022L6.1217 3Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.565 10.3794C15.3324 10.1419 15.0185 10.0059 14.6895 10.0002C14.3605 9.99443 14.0422 10.1193 13.8018 10.3485C13.7897 10.3602 13.7801 10.3743 13.7736 10.3899C13.767 10.4055 13.7636 10.4223 13.7636 10.4393C13.7636 10.4562 13.767 10.473 13.7736 10.4887C13.7801 10.5043 13.7897 10.5183 13.8018 10.53L13.9547 10.6898C13.9754 10.7107 14.0028 10.7234 14.0319 10.7254C14.0609 10.7274 14.0898 10.7186 14.113 10.7007C14.2597 10.5811 14.4459 10.524 14.6329 10.5414C14.8198 10.5587 14.9929 10.6492 15.116 10.7938C15.2391 10.9385 15.3027 11.1261 15.2934 11.3174C15.2841 11.5088 15.2027 11.6891 15.0662 11.8206C14.6477 12.2474 14.171 11.9997 13.5378 11.6708C12.647 11.2079 11.4465 10.5842 9.66257 11.4521C9.61107 11.4774 9.56524 11.5132 9.52801 11.5575C9.49077 11.6017 9.46292 11.6533 9.4462 11.7091C9.42949 11.7649 9.42428 11.8236 9.43092 11.8815C9.43755 11.9394 9.45587 11.9953 9.48474 12.0457L10.0085 12.9687C10.0373 13.0199 10.0761 13.0638 10.1218 13.0981C10.1325 13.1059 10.1437 13.1131 10.1551 13.1198C10.2177 13.1565 10.2883 13.1754 10.3598 13.1752L10.3613 13.1752C10.4297 13.1753 10.4972 13.1575 10.5574 13.123L10.5609 13.121C10.9107 12.9279 11.231 12.7004 11.5244 12.4307C11.6261 12.3372 11.7842 12.3439 11.8777 12.4455C11.9711 12.5471 11.9645 12.7053 11.8628 12.7987C11.539 13.0965 11.1863 13.3468 10.8042 13.5578C10.669 13.6348 10.5164 13.6754 10.3609 13.6752C10.1998 13.6756 10.0417 13.6328 9.90255 13.5513C9.76323 13.4698 9.64805 13.3526 9.56811 13.2121L9.09541 12.3796C8.27149 13.1021 7.7946 14.3497 8.08603 15.8992C8.0912 15.9274 8.10583 15.9529 8.12742 15.9713C8.149 15.9897 8.17619 15.9998 8.2043 16H8.76271C8.79347 16.0008 8.82338 15.9896 8.84635 15.9687C8.86931 15.9478 8.8836 15.9187 8.88631 15.8874C8.91254 15.6847 9.01007 15.4986 9.16073 15.3638C9.31138 15.229 9.5049 15.1547 9.70525 15.1547C9.9056 15.1547 10.0991 15.229 10.2498 15.3638C10.4004 15.4986 10.498 15.6847 10.5242 15.8874C10.5277 15.9172 10.5417 15.9446 10.5637 15.9645C10.5856 15.9844 10.614 15.9954 10.6433 15.9954H11.1893C11.2186 15.9952 11.2469 15.9841 11.2688 15.9643C11.2907 15.9444 11.3048 15.9171 11.3085 15.8874C11.3347 15.6847 11.4322 15.4986 11.5829 15.3638C11.7336 15.229 11.9271 15.1547 12.1274 15.1547C12.3278 15.1547 12.5213 15.229 12.6719 15.3638C12.8226 15.4986 12.9201 15.6847 12.9464 15.8874C12.95 15.9171 12.9641 15.9444 12.986 15.9643C13.008 15.9841 13.0362 15.9952 13.0655 15.9954H13.6079C13.6397 15.9954 13.6702 15.9827 13.6928 15.9599C13.7154 15.9372 13.7284 15.9062 13.7288 15.8738C13.7413 15.0933 13.9476 14.1966 14.5354 13.7474C16.5716 12.1927 16.0363 10.8604 15.565 10.3794ZM12.6279 12.7721L13.4922 13.2142C13.5665 13.158 13.6256 13.0853 13.6637 12.9993C13.7018 12.9132 13.7176 12.8186 13.7097 12.7245C13.7018 12.6304 13.6705 12.5399 13.6186 12.4617C13.5667 12.3835 13.4961 12.3201 13.4135 12.2776C13.3308 12.2352 13.2389 12.215 13.1464 12.2191C13.054 12.2232 12.9641 12.2513 12.8853 12.3009C12.8066 12.3505 12.7416 12.4198 12.6965 12.5023C12.6515 12.5848 12.6278 12.6777 12.6279 12.7721Z" fill="#548AF7"/>
<path d="M1 3.86667C1 2.83574 1.73127 2 2.63333 2H6.1217C6.39413 2 6.65477 2.11115 6.84336 2.30775L8.46667 4L13 4C14.1046 4 15 4.89543 15 6V9.00019H14.6975C14.1063 8.99248 13.5387 9.21829 13.1124 9.62462L13.1072 9.62955L13.1055 9.63119C12.9964 9.73724 12.9105 9.86373 12.8522 10.0024C12.8195 10.0802 12.7957 10.1612 12.7812 10.2439C12.5697 10.1751 12.3421 10.1162 12.0993 10.0759C11.2547 9.93571 10.2982 10.031 9.22485 10.5533L9.22277 10.5543C9.04568 10.6411 8.88945 10.7639 8.76346 10.9135C8.63751 11.0631 8.54439 11.2366 8.48877 11.4221C8.46921 11.4874 8.4544 11.5539 8.44437 11.621L8.4366 11.6278C7.76612 12.2157 7.29304 13.0363 7.0988 14H2.63333C1.73127 14 1 13.1643 1 12.1333V3.86667Z" fill="#43454A"/>
<path d="M6.1217 3L2.63333 3C2.40481 3 2 3.25841 2 3.86667V12.1333C2 12.7416 2.40481 13 2.63333 13H7.42186C7.27921 13.311 7.17018 13.6459 7.0988 14H2.63333C1.73127 14 1 13.1643 1 12.1333V3.86667C1 2.83574 1.73127 2 2.63333 2H6.1217C6.39413 2 6.65477 2.11115 6.84336 2.30775L8.46667 4L13 4C14.1046 4 15 4.89543 15 6V9.00019H14.6975C14.4586 8.99708 14.2235 9.03211 14 9.10179V6C14 5.44771 13.5523 5 13 5H8.04022L6.1217 3Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8944 3.54486C14.5319 2.78856 13.3381 1.8948 12.0006 3.06357M14.8944 3.54486C15.3569 4.42607 14.5631 6.06982 12.9631 6.06982C10.8767 6.06982 8.68787 2.61277 3.9237 4.96165C3.50279 5.16918 3.31562 5.6629 3.45956 6.10957L3.57564 6.46982M14.8944 3.54486C14.8944 3.54486 16.2295 6.36001 12.5643 9.10175C11.5314 9.87523 11.153 11.3935 11.1147 12.7459C11.1132 12.8004 11.0688 12.8441 11.0142 12.8441H9.87321C9.77409 12.8441 9.69136 12.771 9.66556 12.6753C9.4931 12.0357 8.90943 11.5836 8.22997 11.5836C7.55227 11.5836 6.96987 12.0398 6.79573 12.6779C6.76971 12.7732 6.68713 12.8459 6.58832 12.8459H5.51173C5.41292 12.8459 5.33034 12.7732 5.30432 12.6779C5.13019 12.0398 4.54778 11.5836 3.87008 11.5836C3.19218 11.5836 2.60962 12.0401 2.43568 12.6784C2.40969 12.7738 2.32709 12.8466 2.22825 12.8466H1.03554C0.984575 12.8466 0.941657 12.8077 0.936422 12.757C0.642735 9.91312 1.46647 7.80583 3.57564 6.46982M3.57564 6.46982L4.15687 8.38056C4.37284 9.01618 5.24066 9.12128 5.77023 8.82391C6.42801 8.46822 7.03442 8.04701 7.59165 7.54507" stroke="#CED0D6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.25933 7.32028C9.16406 7.25907 9.1068 7.14753 9.14402 7.04058C9.24352 6.75468 9.50016 6.54346 9.8117 6.5096C10.1856 6.46897 10.5372 6.69485 10.6556 7.05184C10.7543 7.34927 10.6688 7.67048 10.4501 7.8798C10.3683 7.9581 10.2431 7.95237 10.1478 7.89115L9.25933 7.32028Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+8
View File
@@ -0,0 +1,8 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10H14.6445L12.6748 11.9697C12.3819 12.2626 12.3819 12.7374 12.6748 13.0303L14.6445 15H10V10Z" fill="#FAF5FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 10C9 9.44771 9.44772 9 10 9H15.248C15.9162 9 16.2508 9.80786 15.7783 10.2803L13.5587 12.5L15.7783 14.7197C16.2508 15.1921 15.9162 16 15.248 16H10C9.44772 16 9 15.5523 9 15V10ZM14.6445 10L10 10V15H14.6445L12.6748 13.0303C12.3819 12.7374 12.3819 12.2626 12.6748 11.9697L14.6445 10Z" fill="#834DF0"/>
<path d="M8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8H10C8.89543 8 8 8.89543 8 10V15Z" fill="#FFF4EB"/>
<path d="M8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8H15C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15V14Z" fill="#E66D17"/>
<path d="M11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11V10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8H11Z" fill="#E66D17"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,8 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 10H14.6445L12.6748 11.9697C12.3819 12.2626 12.3819 12.7374 12.6748 13.0303L14.6445 15H10V10Z" fill="#2F2936"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 10C9 9.44771 9.44772 9 10 9H15.248C15.9162 9 16.2508 9.80786 15.7783 10.2803L13.5587 12.5L15.7783 14.7197C16.2508 15.1921 15.9162 16 15.248 16H10C9.44772 16 9 15.5523 9 15V10ZM14.6445 10L10 10V15H14.6445L12.6748 13.0303C12.3819 12.7374 12.3819 12.2626 12.6748 11.9697L14.6445 10Z" fill="#A571E6"/>
<path d="M8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8H10C8.89543 8 8 8.89543 8 10V15Z" fill="#45322B"/>
<path d="M8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8H15C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15V14Z" fill="#C77D55"/>
<path d="M11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11V10C6.89543 10 6 9.10457 6 8C6 6.89543 6.89543 6 8 6C9.10457 6 10 6.89543 10 8H11Z" fill="#C77D55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+6
View File
@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9497 3.05025C15.6834 5.78392 15.6834 10.2161 12.9497 12.9497C10.216 15.6834 5.78388 15.6834 3.05021 12.9497C0.316541 10.2161 0.316541 5.78392 3.05021 3.05025C5.78388 0.316583 10.216 0.316583 12.9497 3.05025Z" fill="#E7EFFD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9143 6.90481L13.9264 7.06045C13.7359 5.85124 13.1755 4.69027 12.2426 3.75736C11.3097 2.82445 10.1487 2.26404 8.93951 2.07352L9.09515 1.0857C10.5062 1.30802 11.8623 1.96287 12.9497 3.05025C14.0371 4.13763 14.6919 5.49375 14.9143 6.90481ZM6.90477 1.0857L7.06041 2.07352C5.8512 2.26404 4.69023 2.82445 3.75732 3.75736C2.82441 4.69027 2.26399 5.85124 2.07348 7.06045L1.08566 6.90481C1.30798 5.49375 1.96283 4.13763 3.05021 3.05025C4.13759 1.96287 5.49371 1.30802 6.90477 1.0857ZM1.08566 9.09519C1.30798 10.5063 1.96283 11.8624 3.05021 12.9497C4.13759 14.0371 5.49371 14.692 6.90477 14.9143L7.0604 13.9265C5.8512 13.736 4.69023 13.1755 3.75732 12.2426C2.82441 11.3097 2.26399 10.1488 2.07348 8.93955L1.08566 9.09519ZM9.09515 14.9143L8.93951 13.9265C10.1487 13.736 11.3097 13.1755 12.2426 12.2426C13.1755 11.3097 13.7359 10.1488 13.9264 8.93955L14.9143 9.09519C14.6919 10.5063 14.0371 11.8624 12.9497 12.9497C11.8623 14.0371 10.5062 14.692 9.09515 14.9143Z" fill="#3574F0"/>
<path d="M8.13295 11.5C9.61223 11.5 10.8836 10.6105 11.2075 9.33909H10.2213C9.90229 10.0739 9.11914 10.6057 8.13295 10.6057C6.77936 10.6057 5.80284 9.51796 5.80284 8C5.80284 6.48204 6.77936 5.39434 8.13295 5.39434C9.11914 5.39434 9.90229 5.92611 10.2213 6.66091H11.2075C10.8836 5.3895 9.61223 4.5 8.13295 4.5C6.21859 4.5 4.79248 5.99378 4.79248 8C4.79248 10.0062 6.21859 11.5 8.13295 11.5Z" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9497 3.05025C15.6834 5.78392 15.6834 10.2161 12.9497 12.9497C10.216 15.6834 5.78388 15.6834 3.05021 12.9497C0.316541 10.2161 0.316541 5.78392 3.05021 3.05025C5.78388 0.316583 10.216 0.316583 12.9497 3.05025Z" fill="#25324D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9143 6.90481L13.9264 7.06045C13.7359 5.85124 13.1755 4.69027 12.2426 3.75736C11.3097 2.82445 10.1487 2.26404 8.93951 2.07352L9.09515 1.0857C10.5062 1.30802 11.8623 1.96287 12.9497 3.05025C14.0371 4.13763 14.6919 5.49375 14.9143 6.90481ZM6.90477 1.0857L7.06041 2.07352C5.8512 2.26404 4.69023 2.82445 3.75732 3.75736C2.82441 4.69027 2.26399 5.85124 2.07348 7.06045L1.08566 6.90481C1.30798 5.49375 1.96283 4.13763 3.05021 3.05025C4.13759 1.96287 5.49371 1.30802 6.90477 1.0857ZM1.08566 9.09519C1.30798 10.5063 1.96283 11.8624 3.05021 12.9497C4.13759 14.0371 5.49371 14.692 6.90477 14.9143L7.0604 13.9265C5.8512 13.736 4.69023 13.1755 3.75732 12.2426C2.82441 11.3097 2.26399 10.1488 2.07348 8.93955L1.08566 9.09519ZM9.09515 14.9143L8.93951 13.9265C10.1487 13.736 11.3097 13.1755 12.2426 12.2426C13.1755 11.3097 13.7359 10.1488 13.9264 8.93955L14.9143 9.09519C14.6919 10.5063 14.0371 11.8624 12.9497 12.9497C11.8623 14.0371 10.5062 14.692 9.09515 14.9143Z" fill="#548AF7"/>
<path d="M8.13295 11.5C9.61223 11.5 10.8836 10.6105 11.2075 9.33909H10.2213C9.90229 10.0739 9.11914 10.6057 8.13295 10.6057C6.77936 10.6057 5.80284 9.51796 5.80284 8C5.80284 6.48204 6.77936 5.39434 8.13295 5.39434C9.11914 5.39434 9.90229 5.92611 10.2213 6.66091H11.2075C10.8836 5.3895 9.61223 4.5 8.13295 4.5C6.21859 4.5 4.79248 5.99378 4.79248 8C4.79248 10.0062 6.21859 11.5 8.13295 11.5Z" fill="#548AF7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7071 9C11.4419 9 11.1875 9.10536 11 9.29289L8.64645 11.6464C8.55268 11.7402 8.5 11.8674 8.5 12C8.5 12.1326 8.55268 12.2598 8.64645 12.3536L9.64645 13.3536C9.84171 13.5488 10.1583 13.5488 10.3536 13.3536L11.5 12.2071L15.1464 15.8536C15.3417 16.0488 15.6583 16.0488 15.8536 15.8536C16.0488 15.6583 16.0488 15.3417 15.8536 15.1464L12.2071 11.5L13.8536 9.85355C13.9966 9.71055 14.0393 9.4955 13.9619 9.30866C13.8845 9.12182 13.7022 9 13.5 9H11.7071Z" fill="#208A3C"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V8H11.707C11.1766 8.00002 10.668 8.21088 10.293 8.58594L7.93945 10.9395C7.65815 11.2208 7.5 11.6022 7.5 12C7.5 12.3978 7.65815 12.7792 7.93945 13.0605L8.87891 14H2.63379C1.73186 14 1.00022 13.1645 1 12.1338V3.86621C1.00022 2.83549 1.73186 2 2.63379 2H6.12207Z" fill="#EBECF0"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V8H14V6C14 5.44771 13.5523 5 13 5H8.04004L6.12207 3H2.63379C2.40532 3 2.0002 3.25829 2 3.86621V12.1338C2.0002 12.7417 2.40532 13 2.63379 13H7.88477C7.90289 13.0202 7.92016 13.0413 7.93945 13.0605L8.87891 14H2.63379L2.4668 13.9902C1.6979 13.9012 1.08689 13.2029 1.00879 12.3242L1 12.1338V3.86621C1.0002 2.89975 1.6431 2.10511 2.4668 2.00977L2.63379 2H6.12207Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7071 9C11.4419 9 11.1875 9.10536 11 9.29289L8.64645 11.6464C8.55268 11.7402 8.5 11.8674 8.5 12C8.5 12.1326 8.55268 12.2598 8.64645 12.3536L9.64645 13.3536C9.84171 13.5488 10.1583 13.5488 10.3536 13.3536L11.5 12.2071L15.1464 15.8536C15.3417 16.0488 15.6583 16.0488 15.8536 15.8536C16.0488 15.6583 16.0488 15.3417 15.8536 15.1464L12.2071 11.5L13.8536 9.85355C13.9966 9.71055 14.0393 9.4955 13.9619 9.30866C13.8845 9.12182 13.7022 9 13.5 9H11.7071Z" fill="#57965C"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V8H11.707C11.1766 8.00002 10.668 8.21088 10.293 8.58594L7.93945 10.9395C7.65815 11.2208 7.5 11.6022 7.5 12C7.5 12.3978 7.65815 12.7792 7.93945 13.0605L8.87891 14H2.63379C1.73186 14 1.00022 13.1645 1 12.1338V3.86621C1.00022 2.83549 1.73186 2 2.63379 2H6.12207Z" fill="#43454A"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V8H14V6C14 5.44771 13.5523 5 13 5H8.04004L6.12207 3H2.63379C2.40532 3 2.0002 3.25829 2 3.86621V12.1338C2.0002 12.7417 2.40532 13 2.63379 13H7.88477C7.90289 13.0202 7.92016 13.0413 7.93945 13.0605L8.87891 14H2.63379L2.4668 13.9902C1.6979 13.9012 1.08689 13.2029 1.00879 12.3242L1 12.1338V3.86621C1.0002 2.89975 1.6431 2.10511 2.4668 2.00977L2.63379 2H6.12207Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" fill="#FFFAEB" stroke="#C27D04"/>
<path d="M9 4.5L6 8H10L7 11.5" stroke="#C27D04" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" fill="#3D3223" stroke="#D6AE58"/>
<path d="M9 4.5L6 8H10L7 11.5" stroke="#D6AE58" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

+12
View File
@@ -0,0 +1,12 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V8.75777C14.285 8.27914 13.4251 8 12.5 8C10.0147 8 8 10.0147 8 12.5C8 13.026 8.09023 13.5308 8.25606 14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#E7EFFD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80265 4.71673L8.09379 5H8.5H13C13.5523 5 14 5.44772 14 6V8.25606C14.356 8.3819 14.6915 8.55127 15 8.75777V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H8.25606C8.14352 13.6816 8.06579 13.3467 8.02746 13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L7.80265 4.71673Z" fill="#3574F0"/>
<path d="M13.5 12.5C13.5 13.0523 13.0523 13.5 12.5 13.5C11.9477 13.5 11.5 13.0523 11.5 12.5C11.5 11.9477 11.9477 11.5 12.5 11.5C13.0523 11.5 13.5 11.9477 13.5 12.5Z" fill="#3574F0"/>
<rect x="12" y="9" width="1" height="2" rx="0.5" fill="#3574F0"/>
<rect x="12" y="14" width="1" height="2" rx="0.5" fill="#3574F0"/>
<rect x="9.21899" y="11.1831" width="1" height="2" rx="0.5" transform="rotate(-60 9.21899 11.1831)" fill="#3574F0"/>
<rect x="9.71899" y="14.6831" width="1" height="2" rx="0.5" transform="rotate(-120 9.71899 14.6831)" fill="#3574F0"/>
<rect x="13.5491" y="13.6831" width="1" height="2" rx="0.5" transform="rotate(-60 13.5491 13.6831)" fill="#3574F0"/>
<rect x="14.0491" y="12.1831" width="1" height="2" rx="0.5" transform="rotate(-120 14.0491 12.1831)" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V8.17071C14.6872 8.06015 14.3506 8 14 8H11C9.34315 8 8 9.34315 8 11V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#25324D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80265 4.71673L8.09379 5H8.5H13C13.5523 5 14 5.44772 14 6V8C14.3506 8 14.6872 8.06015 15 8.17071V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H8V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L7.80265 4.71673Z" fill="#3574F0"/>
<path d="M13.5 12.5C13.5 13.0523 13.0523 13.5 12.5 13.5C11.9477 13.5 11.5 13.0523 11.5 12.5C11.5 11.9477 11.9477 11.5 12.5 11.5C13.0523 11.5 13.5 11.9477 13.5 12.5Z" fill="#3574F0"/>
<rect x="12" y="9" width="1" height="2" rx="0.5" fill="#3574F0"/>
<rect x="12" y="14" width="1" height="2" rx="0.5" fill="#3574F0"/>
<rect x="9.21899" y="11.1831" width="1" height="2" rx="0.5" transform="rotate(-60 9.21899 11.1831)" fill="#3574F0"/>
<rect x="9.71899" y="14.6831" width="1" height="2" rx="0.5" transform="rotate(-120 9.71899 14.6831)" fill="#3574F0"/>
<rect x="13.5491" y="13.6831" width="1" height="2" rx="0.5" transform="rotate(-60 13.5491 13.6831)" fill="#3574F0"/>
<rect x="14.0491" y="12.1831" width="1" height="2" rx="0.5" transform="rotate(-120 14.0491 12.1831)" fill="#3574F0"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+12
View File
@@ -0,0 +1,12 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V8.75777C14.285 8.27914 13.4251 8 12.5 8C10.0147 8 8 10.0147 8 12.5C8 13.026 8.09023 13.5308 8.25606 14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#F2FCF3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80265 4.71673L8.09379 5H8.5H13C13.5523 5 14 5.44772 14 6V8.25606C14.356 8.3819 14.6915 8.55127 15 8.75777V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H8.25606C8.14352 13.6816 8.06579 13.3467 8.02746 13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L7.80265 4.71673Z" fill="#208A3C"/>
<path d="M13.5 12.5C13.5 13.0523 13.0523 13.5 12.5 13.5C11.9477 13.5 11.5 13.0523 11.5 12.5C11.5 11.9477 11.9477 11.5 12.5 11.5C13.0523 11.5 13.5 11.9477 13.5 12.5Z" fill="#208A3C"/>
<rect x="12" y="9" width="1" height="2" rx="0.5" fill="#208A3C"/>
<rect x="12" y="14" width="1" height="2" rx="0.5" fill="#208A3C"/>
<rect x="9.21899" y="11.1831" width="1" height="2" rx="0.5" transform="rotate(-60 9.21899 11.1831)" fill="#208A3C"/>
<rect x="9.71899" y="14.6832" width="1" height="2" rx="0.5" transform="rotate(-120 9.71899 14.6832)" fill="#208A3C"/>
<rect x="13.5491" y="13.6831" width="1" height="2" rx="0.5" transform="rotate(-60 13.5491 13.6831)" fill="#208A3C"/>
<rect x="14.0491" y="12.1831" width="1" height="2" rx="0.5" transform="rotate(-120 14.0491 12.1831)" fill="#208A3C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V8.17071C14.6872 8.06015 14.3506 8 14 8H11C9.34315 8 8 9.34315 8 11V14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#253627"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80265 4.71673L8.09379 5H8.5H13C13.5523 5 14 5.44772 14 6V8C14.3506 8 14.6872 8.06015 15 8.17071V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H8V13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823L7.80265 4.71673Z" fill="#57965C"/>
<path d="M13.5 12.5C13.5 13.0523 13.0523 13.5 12.5 13.5C11.9477 13.5 11.5 13.0523 11.5 12.5C11.5 11.9477 11.9477 11.5 12.5 11.5C13.0523 11.5 13.5 11.9477 13.5 12.5Z" fill="#57965C"/>
<rect x="12" y="9" width="1" height="2" rx="0.5" fill="#57965C"/>
<rect x="12" y="14" width="1" height="2" rx="0.5" fill="#57965C"/>
<rect x="9.21899" y="11.1831" width="1" height="2" rx="0.5" transform="rotate(-60 9.21899 11.1831)" fill="#57965C"/>
<rect x="9.71899" y="14.6832" width="1" height="2" rx="0.5" transform="rotate(-120 9.71899 14.6832)" fill="#57965C"/>
<rect x="13.5491" y="13.6831" width="1" height="2" rx="0.5" transform="rotate(-60 13.5491 13.6831)" fill="#57965C"/>
<rect x="14.0491" y="12.1831" width="1" height="2" rx="0.5" transform="rotate(-120 14.0491 12.1831)" fill="#57965C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+6
View File
@@ -0,0 +1,6 @@
<!-- Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6723 16.0901C12.0655 16.0901 11.5845 15.9234 11.2291 15.5899C10.8793 15.2564 10.7043 14.8027 10.7043 14.2287H11.7293C11.7293 14.5294 11.8113 14.7644 11.9753 14.9339C12.1448 15.1034 12.3771 15.1881 12.6723 15.1881C12.9675 15.1881 13.1971 15.1034 13.3611 14.9339C13.5306 14.7644 13.6153 14.5321 13.6153 14.2369V10.9651H12.2213V10.0139H14.6403V14.2369C14.6403 14.8054 14.4627 15.2564 14.1073 15.5899C13.7575 15.9234 13.2791 16.0901 12.6723 16.0901Z" fill="#3574F0"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V9H11C10.4477 9 10 9.44772 10 10V14H2.63379C1.73186 14 1.00022 13.1645 1 12.1338V3.86621C1.00022 2.83549 1.73186 2 2.63379 2H6.12207Z" fill="#EBECF0"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V9H14V6C14 5.44771 13.5523 5 13 5H8.04004L6.12207 3H2.63379C2.40532 3 2.0002 3.25829 2 3.86621V12.1338C2.0002 12.7417 2.40532 13 2.63379 13H10V14H2.63379L2.4668 13.9902C1.6979 13.9012 1.08689 13.2029 1.00879 12.3242L1 12.1338V3.86621C1.0002 2.89975 1.6431 2.10511 2.4668 2.00977L2.63379 2H6.12207Z" fill="#6C707E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6723 16.0901C12.0655 16.0901 11.5845 15.9234 11.2291 15.5899C10.8793 15.2564 10.7043 14.8027 10.7043 14.2287H11.7293C11.7293 14.5294 11.8113 14.7644 11.9753 14.9339C12.1448 15.1034 12.3771 15.1881 12.6723 15.1881C12.9675 15.1881 13.1971 15.1034 13.3611 14.9339C13.5306 14.7644 13.6153 14.5321 13.6153 14.2369V10.9651H12.2213V10.0139H14.6403V14.2369C14.6403 14.8054 14.4627 15.2564 14.1073 15.5899C13.7575 15.9234 13.2791 16.0901 12.6723 16.0901Z" fill="#548AF7"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V9H11C10.4477 9 10 9.44772 10 10V14H2.63379C1.73186 14 1.00022 13.1645 1 12.1338V3.86621C1.00022 2.83549 1.73186 2 2.63379 2H6.12207Z" fill="#43454A"/>
<path d="M6.12207 2C6.39437 2.0001 6.65525 2.11111 6.84375 2.30762L8.4668 4H13C14.1046 4 15 4.89543 15 6V9H14V6C14 5.44771 13.5523 5 13 5H8.04004L6.12207 3H2.63379C2.40532 3 2.0002 3.25829 2 3.86621V12.1338C2.0002 12.7417 2.40532 13 2.63379 13H10V14H2.63379L2.4668 13.9902C1.6979 13.9012 1.08689 13.2029 1.00879 12.3242L1 12.1338V3.86621C1.0002 2.89975 1.6431 2.10511 2.4668 2.00977L2.63379 2H6.12207Z" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+12
View File
@@ -0,0 +1,12 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_12388_792)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.50215C9 0.324411 10.2951 -0.393923 11.2942 0.229668L15.2962 2.7275C16.2372 3.31485 16.2372 4.68512 15.2962 5.27247L11.2942 7.7703C10.2951 8.39389 9 7.67556 9 6.49782V1.50215ZM10.7647 1.07799C10.4317 0.870127 10 1.10957 10 1.50215V6.49782C10 6.8904 10.4317 7.12985 10.7647 6.92198L14.7667 4.42415C15.0804 4.22836 15.0804 3.77161 14.7667 3.57582L10.7647 1.07799Z" fill="#208A3C"/>
<path d="M10 1.50217C10 1.10959 10.4317 0.870147 10.7647 1.07801L14.7667 3.57584C15.0804 3.77163 15.0804 4.22838 14.7667 4.42417L10.7647 6.922C10.4317 7.12987 10 6.89042 10 6.49784V1.50217Z" fill="#F2FCF3"/>
</g>
<defs>
<clipPath id="clip0_12388_792">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

+12
View File
@@ -0,0 +1,12 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_12388_796)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.50215C9 0.324411 10.2951 -0.393923 11.2942 0.229668L15.2962 2.7275C16.2372 3.31485 16.2372 4.68512 15.2962 5.27247L11.2942 7.7703C10.2951 8.39389 9 7.67556 9 6.49782V1.50215ZM10.7647 1.07799C10.4317 0.870127 10 1.10957 10 1.50215V6.49782C10 6.8904 10.4317 7.12985 10.7647 6.92198L14.7667 4.42415C15.0804 4.22836 15.0804 3.77161 14.7667 3.57582L10.7647 1.07799Z" fill="#57965C"/>
<path d="M10 1.50217C10 1.10959 10.4317 0.870147 10.7647 1.07801L14.7667 3.57584C15.0804 3.77163 15.0804 4.22838 14.7667 4.42417L10.7647 6.922C10.4317 7.12987 10 6.89042 10 6.49784V1.50217Z" fill="#253627"/>
</g>
<defs>
<clipPath id="clip0_12388_796">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" fill="#FFF7F7" stroke="#DB3B4B"/>
<path d="M8.46217 11.5H9.46061V10.5943H8.59311C8.26575 10.5943 8.02023 10.3488 8.02023 10.0214V6.81878H9.46061L9.46606 5.91309H8.02023V4.5H7.07635V5.44934C7.07635 5.72214 6.88539 5.91309 6.6126 5.91309H5.90878L5.90332 6.81878H6.9836L6.98906 10.1142C6.98906 10.9108 7.61104 11.5 8.46217 11.5Z" fill="#DB3B4B"/>
</svg>

After

Width:  |  Height:  |  Size: 604 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" fill="#402929" stroke="#DB5C5C"/>
<path d="M8.46217 11.5H9.46061V10.5943H8.59311C8.26575 10.5943 8.02023 10.3488 8.02023 10.0214V6.81878H9.46061L9.46606 5.91309H8.02023V4.5H7.07635V5.44934C7.07635 5.72214 6.88539 5.91309 6.6126 5.91309H5.90878L5.90332 6.81878H6.9836L6.98906 10.1142C6.98906 10.9108 7.61104 11.5 8.46217 11.5Z" fill="#DB5C5C"/>
</svg>

After

Width:  |  Height:  |  Size: 604 B

+30 -1
View File
@@ -41,6 +41,20 @@ const ICONS = {
"folder-library": "nodes/libraryFolder",
"folder-github": "nodes/folderGithub",
module: "nodes/module",
"folder-generated-source": "nodes/generatedSource",
"folder-generated-test": "nodes/generatedTestRoot",
"folder-compiled": "nodes/compiledClassesFolder",
"folder-javadoc": "nodes/javaDocFolder",
"class-abstract": "nodes/classAbstract",
record: "nodes/record",
annotation: "nodes/annotation",
exception: "nodes/exception",
"jvm-test": "nodes/test",
"runnable-mark": "nodes/runnableMark",
"kotlin-object": "kotlin/objectKotlin",
"spring-config": "openidea/runConfigurations/springBoot",
"gradle-project": "gradle/gradle",
"gradle-module": "gradle/gradleSubproject",
"spring-boot": "openidea/runConfigurations/springBoot",
file: "fileTypes/text",
"file-unknown": "fileTypes/unknown",
@@ -917,7 +931,12 @@ export type FolderKind =
| "excluded"
| "library"
| "github"
| "module";
| "module"
| "package"
| "generatedSource"
| "generatedTest"
| "compiled"
| "javadoc";
/** Folder icon by role (expui has no separate "open" folder glyph). */
export function folderIconName(kind: FolderKind = "plain"): IconName {
@@ -938,6 +957,16 @@ export function folderIconName(kind: FolderKind = "plain"): IconName {
return "folder-github";
case "module":
return "module";
case "package":
return "package";
case "generatedSource":
return "folder-generated-source";
case "generatedTest":
return "folder-generated-test";
case "compiled":
return "folder-compiled";
case "javadoc":
return "folder-javadoc";
default:
return "folder";
}