File icons (#2719)

Mikayla Maki created

This PR adds the next most requested editor feature.

TODO:
- [x] Figure out styles and icons for supported file types with

fixes https://github.com/zed-industries/community/issues/206

Release Notes:

- Added file icons

Change summary

Cargo.lock                                         |   1 
assets/icons/file_icons/archive.svg                |   5 
assets/icons/file_icons/book.svg                   |   4 
assets/icons/file_icons/camera.svg                 |   4 
assets/icons/file_icons/code.svg                   |   4 
assets/icons/file_icons/database.svg               |   5 
assets/icons/file_icons/eslint.svg                 |   4 
assets/icons/file_icons/file.svg                   |   5 
assets/icons/file_icons/file_types.json            | 149 ++++++++++++++++
assets/icons/file_icons/folder-open.svg            |   4 
assets/icons/file_icons/folder.svg                 |   4 
assets/icons/file_icons/git.svg                    |   6 
assets/icons/file_icons/hash.svg                   |   6 
assets/icons/file_icons/html.svg                   |   5 
assets/icons/file_icons/image.svg                  |   6 
assets/icons/file_icons/info.svg                   |   3 
assets/icons/file_icons/lock.svg                   |   6 
assets/icons/file_icons/notebook.svg               |   6 
assets/icons/file_icons/package.svg                |   3 
assets/icons/file_icons/prettier.svg               |  12 +
assets/icons/file_icons/rust.svg                   |   2 
assets/icons/file_icons/settings.svg               |   1 
assets/icons/file_icons/terminal.svg               |   5 
assets/icons/file_icons/typescript.svg             |   2 
assets/settings/default.json                       |   2 
crates/project_panel/Cargo.toml                    |   1 
crates/project_panel/src/file_associations.rs      |  84 +++++++++
crates/project_panel/src/project_panel.rs          |  70 +++++--
crates/project_panel/src/project_panel_settings.rs |   2 
crates/theme/src/theme.rs                          |   4 
crates/zed/src/main.rs                             |  28 ++
crates/zed/src/zed.rs                              |   2 
script/generate-licenses                           |   2 
styles/src/style_tree/project_panel.ts             |  26 +-
34 files changed, 438 insertions(+), 35 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5360,6 +5360,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client",
+ "collections",
  "context_menu",
  "db",
  "drag_and_drop",

assets/icons/file_icons/archive.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 7.63H8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<rect x="2" y="2" width="10" height="3" rx="1" stroke="black" stroke-width="1.25"/>
+<path d="M3 5H11L10.5663 11.0712C10.529 11.5946 10.0935 12 9.56888 12H4.43112C3.90648 12 3.47104 11.5946 3.43366 11.0712L3 5Z" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/file_icons/book.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 2C7.44772 2 7 2.44772 7 3V13C7.5 12 8.5 11.375 10 11.375H11C11.5523 11.375 12 10.9273 12 10.375V3C12 2.44772 11.5523 2 11 2H8Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 2C6.55228 2 7 2.44772 7 3V13C6.5 12 5.5 11.375 4 11.375H3C2.44772 11.375 2 10.9273 2 10.375V3C2 2.44772 2.44772 2 3 2H6Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/camera.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 10.1111C12 10.602 11.593 11 11.0909 11H2.90909C2.40701 11 2 10.602 2 10.1111V5.22222C2 4.7313 2.40701 4.38 2.90909 4.38H4.72727L5.5 3H8.5L9.27273 4.38H11.0909C11.593 4.38 12 4.7313 12 5.22222V10.1111Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10753 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10753 9 7.005 9Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/code.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 2C3.5 2 2.25 3 3 4.5C3.5 5.5 3 6.5 2 6.99894C3 7.5 3.5 8.5 3 9.5C2.25 11 3 12 5 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 2C10.5 2 11.75 3 11 4.5C10.5 5.5 11 6.5 12 6.99894C11 7.5 10.5 8.5 11 9.5C11.75 11 11 12 9 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/database.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<ellipse cx="7" cy="4" rx="5" ry="2" stroke="black" stroke-width="1.25"/>
+<path d="M12 4V10C12 11.1046 9.76142 12 7 12C4.23858 12 2 11.1046 2 10V4" stroke="black" stroke-width="1.25"/>
+<path d="M12 7C12 8.10457 9.76142 9 7 9C4.23858 9 2 8.10457 2 7" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/file_icons/eslint.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.3969 7.5625L11.8557 7.25L12.3969 7.5625C12.5979 7.21442 12.5979 6.78558 12.3969 6.4375L11.8557 6.75L12.3969 6.4375L10.1856 2.60737C9.98464 2.2593 9.61325 2.04487 9.21132 2.04487L4.78868 2.04487C4.38675 2.04487 4.01536 2.2593 3.8144 2.60737L1.60307 6.4375C1.40211 6.78558 1.40211 7.21442 1.60307 7.5625L3.8144 11.3926C4.01536 11.7407 4.38675 11.9551 4.78867 11.9551L9.21132 11.9551C9.61325 11.9551 9.98464 11.7407 10.1856 11.3926L12.3969 7.5625Z" stroke="black" stroke-width="1.25"/>
+<path d="M6.75 4.14434C6.9047 4.05502 7.0953 4.05502 7.25 4.14434L9.34808 5.35566C9.50278 5.44498 9.59808 5.61004 9.59808 5.78868V8.21132C9.59808 8.38996 9.50278 8.55502 9.34808 8.64434L7.25 9.85566C7.0953 9.94498 6.9047 9.94498 6.75 9.85566L4.65192 8.64434C4.49722 8.55502 4.40192 8.38996 4.40192 8.21132L4.40192 5.78868C4.40192 5.61004 4.49722 5.44498 4.65192 5.35566L6.75 4.14434Z" fill="black"/>
+</svg>

assets/icons/file_icons/file.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 4H10" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 7H12" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 10H8" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/file_types.json 🔗

@@ -0,0 +1,149 @@
+{
+  "suffixes": {
+    "aac": "audio",
+    "bash": "terminal",
+    "bmp": "image",
+    "c": "code",
+    "conf": "settings",
+    "cpp": "code",
+    "cc": "code",
+    "css": "code",
+    "doc": "document",
+    "docx": "document",
+    "eslintrc": "eslint",
+    "eslintrc.js": "eslint",
+    "eslintrc.json": "eslint",
+    "flac": "audio",
+    "fish": "terminal",
+    "gitattributes": "vcs",
+    "gitignore": "vcs",
+    "gif": "image",
+    "go": "code",
+    "h": "code",
+    "handlebars": "code",
+    "hbs": "template",
+    "htm": "template",
+    "html": "template",
+    "svelte": "template",
+    "hpp": "code",
+    "ico": "image",
+    "ini": "settings",
+    "java": "code",
+    "jpeg": "image",
+    "jpg": "image",
+    "js": "code",
+    "json": "storage",
+    "lock": "lock",
+    "log": "log",
+    "md": "document",
+    "mdx": "document",
+    "mp3": "audio",
+    "mp4": "video",
+    "ods": "document",
+    "odp": "document",
+    "odt": "document",
+    "ogg": "video",
+    "pdf": "document",
+    "php": "code",
+    "png": "image",
+    "ppt": "document",
+    "pptx": "document",
+    "prettierrc": "prettier",
+    "prettierignore": "prettier",
+    "ps1": "terminal",
+    "psd": "image",
+    "py": "code",
+    "rb": "code",
+    "rkt": "code",
+    "rs": "rust",
+    "rtf": "document",
+    "scm": "code",
+    "sh": "terminal",
+    "bashrc": "terminal",
+    "bash_profile": "terminal",
+    "bash_aliases": "terminal",
+    "bash_logout": "terminal",
+    "profile": "terminal",
+    "zshrc": "terminal",
+    "zshenv": "terminal",
+    "zsh_profile": "terminal",
+    "zsh_aliases": "terminal",
+    "zsh_histfile": "terminal",
+    "zlogin": "terminal",
+    "sql": "code",
+    "svg": "image",
+    "swift": "code",
+    "tiff": "image",
+    "toml": "settings",
+    "ts": "typescript",
+    "tsx": "code",
+    "txt": "document",
+    "wav": "audio",
+    "webm": "video",
+    "xls": "document",
+    "xlsx": "document",
+    "xml": "template",
+    "yaml": "settings",
+    "yml": "settings",
+    "zsh": "terminal"
+  },
+  "types": {
+    "audio": {
+      "icon": "icons/file_icons/file.svg"
+    },
+    "code": {
+      "icon": "icons/file_icons/code.svg"
+    },
+    "default": {
+      "icon": "icons/file_icons/file.svg"
+    },
+    "directory": {
+      "icon": "icons/file_icons/folder.svg"
+    },
+    "document": {
+      "icon": "icons/file_icons/book.svg"
+    },
+    "eslint": {
+      "icon": "icons/file_icons/eslint.svg"
+    },
+    "expanded_directory": {
+      "icon": "icons/file_icons/folder-open.svg"
+    },
+    "image": {
+      "icon": "icons/file_icons/image.svg"
+    },
+    "lock": {
+      "icon": "icons/file_icons/lock.svg"
+    },
+    "log": {
+      "icon": "icons/file_icons/info.svg"
+    },
+    "prettier": {
+      "icon": "icons/file_icons/prettier.svg"
+    },
+    "rust": {
+      "icon": "icons/file_icons/rust.svg"
+    },
+    "settings": {
+      "icon": "icons/file_icons/settings.svg"
+    },
+    "storage": {
+      "icon": "icons/file_icons/database.svg"
+    },
+    "template": {
+      "icon": "icons/file_icons/html.svg"
+    },
+    "terminal": {
+      "icon": "icons/file_icons/terminal.svg"
+    },
+    "typescript": {
+      "icon": "icons/file_icons/typescript.svg"
+    },
+    "vcs": {
+      "icon": "icons/file_icons/git.svg"
+    },
+    "video": {
+      "icon": "icons/file_icons/file.svg"
+    }
+  }
+}

assets/icons/file_icons/folder-open.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.08592 5.30382C4.22115 4.89402 4.60401 4.61719 5.03555 4.61719H11.9295C12.6108 4.61719 13.0926 5.28358 12.8791 5.93056L11.2266 10.9384C11.0914 11.3482 10.7085 11.625 10.2769 11.625H3.38303C2.70174 11.625 2.2199 10.9586 2.4334 10.3116L4.08592 5.30382Z" stroke="black" stroke-width="1.25"/>
+<path d="M8 4.4024L7.27505 2.93264C7.10664 2.59119 6.75894 2.375 6.37821 2.375H3C2.44772 2.375 2 2.82272 2 3.375V4.4024V10.625C2 11.1773 2.44772 11.625 3 11.625H4.00781" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/folder.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 5.625C2 5.07272 2.44772 4.625 3 4.625H11C11.5523 4.625 12 5.07272 12 5.625V10.625C12 11.1773 11.5523 11.625 11 11.625H3C2.44772 11.625 2 11.1773 2 10.625V5.625Z" stroke="black" stroke-width="1.25"/>
+<path d="M8 4.375L7.27639 2.92779C7.107 2.589 6.76074 2.375 6.38197 2.375H3C2.44772 2.375 2 2.82272 2 3.375V8" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/file_icons/git.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="4" cy="10" r="2" stroke="black" stroke-width="1.25"/>
+<circle cx="10" cy="4" r="2" stroke="black" stroke-width="1.25"/>
+<line x1="3.625" y1="2.625" x2="3.625" y2="7.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M10 6V6C10 8.20914 8.20914 10 6 10V10" stroke="black" stroke-width="1.25"/>
+</svg>

assets/icons/file_icons/hash.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<line x1="10.2795" y1="2.63847" x2="7.74785" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="6.26624" y1="2.99597" x2="3.7346" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="3.15982" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<line x1="2.0983" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/html.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.15732 3.17108L5.84268 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/image.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.5 3C6.91421 3 7.25 2.66421 7.25 2.25C7.25 1.83579 6.91421 1.5 6.5 1.5C6.08579 1.5 5.75 1.83579 5.75 2.25C5.75 2.66421 6.08579 3 6.5 3Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.375 2H3C2.44772 2 2 2.44772 2 3V11C2 11.5523 2.44772 12 3 12H7.35938M9.64062 2H11C11.5523 2 12 2.44772 12 3V11C12 11.5523 11.5523 12 11 12H10.125" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 10L5 7L7 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8L9 5L12 8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/info.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 7L7 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 5.25C7.34518 5.25 7.625 4.97018 7.625 4.625C7.625 4.27982 7.34518 4 7 4C6.65482 4 6.375 4.27982 6.375 4.625C6.375 4.97018 6.65482 5.25 7 5.25Z" fill="black"/>

assets/icons/file_icons/lock.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="3" y="5" width="8" height="7" rx="1" stroke="black" stroke-width="1.35"/>
+<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-width="1.35"/>
+<circle cx="7" cy="8" r="1" fill="black"/>
+<path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/notebook.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.4 12H3.59997C2.71633 12 2 11.3367 2 10.5185V3.48148C2 2.66328 2.71633 2 3.59997 2H10.4C11.2837 2 12 2.66328 12 3.48148V10.5185C12 11.3367 11.2837 12 10.4 12Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 5L7.5 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 7H7.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/package.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1.62679 3.88473L6.99984 6.78517M1.62679 3.88473L1.63138 9.90006L7.00444 12.8005M1.62679 3.88473L4.31118 2.54212M6.99984 6.78517L7.00444 12.8005M6.99984 6.78517L9.68415 5.33085M7.00444 12.8005L12.3731 9.89186L12.3685 3.87652M4.31118 2.54212L6.99558 1.1995L12.3685 3.87652M4.31118 2.54212L9.68415 5.33085M12.3685 3.87652L9.68415 5.33085" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/file_icons/prettier.svg 🔗

@@ -0,0 +1,12 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 2.86328H8.51563" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M11 2.86328L12 2.86328" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M4.79688 5.6263L7.15625 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M7.15625 8.3737L12 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M2 11.1094H3.54687" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M5.97656 11.1094H8.35938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/rust.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.27935 9.98207C4.32063 9.4038 3.9204 8.89049 3.35998 8.80276L2.60081 8.68387C2.37979 8.64945 2.20167 8.48001 2.15225 8.25614L2.01378 7.63511C1.96382 7.41235 2.05233 7.1807 2.23696 7.05125L2.8631 6.61242C3.33337 6.28297 3.47456 5.6369 3.18621 5.13364L2.79467 4.45092C2.68118 4.25261 2.69801 4.00374 2.83757 3.82321L3.22314 3.32436C3.3627 3.14438 3.59621 3.06994 3.81071 3.13772L4.57531 3.37769C5.11944 3.54879 5.70048 3.26159 5.90683 2.71886L6.1811 1.99782C6.26255 1.78395 6.46345 1.64285 6.68772 1.6423L7.31007 1.64063C7.53434 1.64007 7.73579 1.78006 7.81834 1.99337L8.09965 2.72275C8.30821 3.26214 8.88655 3.54712 9.42903 3.37714L10.1632 3.14716C10.3772 3.07994 10.6096 3.15382 10.7492 3.3327L11.1374 3.83099" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>

assets/icons/file_icons/terminal.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="1.65625" y="2" width="10.6875" height="10" rx="1" stroke="black" stroke-width="1.25"/>
+<path d="M4.375 9L6.375 7L4.375 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.625 9L9.90625 9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/file_icons/typescript.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 4.375V3C12 2.44772 11.5523 2 11 2H3C2.44772 2 2 2.44772 2 3V11C2 11.5523 2.44772 12 3 12H3.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>

assets/settings/default.json 🔗

@@ -99,6 +99,8 @@
   "project_panel": {
     // Whether to show the git status in the project panel.
     "git_status": true,
+    // Whether to show file icons in the project panel.
+    "file_icons": true,
     // Where to dock project panel. Can be 'left' or 'right'.
     "dock": "left",
     // Default width of the project panel.

crates/project_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 context_menu = { path = "../context_menu" }
+collections = { path = "../collections" }
 db = { path = "../db" }
 drag_and_drop = { path = "../drag_and_drop" }
 editor = { path = "../editor" }

crates/project_panel/src/file_associations.rs 🔗

@@ -0,0 +1,84 @@
+use std::{path::Path, str, sync::Arc};
+
+use collections::HashMap;
+
+use gpui::{AppContext, AssetSource};
+use serde_derive::Deserialize;
+use util::iife;
+
+#[derive(Deserialize, Debug)]
+struct TypeConfig {
+    icon: Arc<str>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct FileAssociations {
+    suffixes: HashMap<String, String>,
+    types: HashMap<String, TypeConfig>,
+}
+
+const DIRECTORY_TYPE: &'static str = "directory";
+const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_directory";
+pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
+
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(FileAssociations::new(assets))
+}
+
+impl FileAssociations {
+    pub fn new(assets: impl AssetSource) -> Self {
+        assets
+            .load("icons/file_icons/file_types.json")
+            .and_then(|file| {
+                serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
+                    .map_err(Into::into)
+            })
+            .unwrap_or_else(|_| FileAssociations {
+                suffixes: HashMap::default(),
+                types: HashMap::default(),
+            })
+    }
+
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
+        iife!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            // FIXME: Associate a type with the languages and have the file's langauge
+            //        override these associations
+            iife!({
+                let suffix = path
+                    .file_name()
+                    .and_then(|os_str| os_str.to_str())
+                    .and_then(|file_name| {
+                        file_name
+                            .find('.')
+                            .and_then(|dot_index| file_name.get(dot_index + 1..))
+                    })?;
+
+                this.suffixes
+                    .get(suffix)
+                    .and_then(|type_str| this.types.get(type_str))
+                    .map(|type_config| type_config.icon.clone())
+            })
+            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        iife!({
+            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+
+            let key = if expanded {
+                EXPANDED_DIRECTORY_TYPE
+            } else {
+                DIRECTORY_TYPE
+            };
+
+            this.types
+                .get(key)
+                .map(|type_config| type_config.icon.clone())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+}

crates/project_panel/src/project_panel.rs 🔗

@@ -1,9 +1,12 @@
+pub mod file_associations;
 mod project_panel_settings;
 
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
 use drag_and_drop::{DragAndDrop, Draggable};
 use editor::{Cancel, Editor};
+use file_associations::FileAssociations;
+
 use futures::stream::StreamExt;
 use gpui::{
     actions,
@@ -15,8 +18,8 @@ use gpui::{
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle,
-    Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity,
+    ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -94,6 +97,7 @@ pub enum ClipboardEntry {
 #[derive(Debug, PartialEq, Eq)]
 pub struct EntryDetails {
     filename: String,
+    icon: Option<Arc<str>>,
     path: Arc<Path>,
     depth: usize,
     kind: EntryKind,
@@ -129,8 +133,9 @@ pub fn init_settings(cx: &mut AppContext) {
     settings::register::<ProjectPanelSettings>(cx);
 }
 
-pub fn init(cx: &mut AppContext) {
+pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     init_settings(cx);
+    file_associations::init(assets, cx);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
     cx.add_action(ProjectPanel::select_prev);
@@ -230,6 +235,11 @@ impl ProjectPanel {
             })
             .detach();
 
+            cx.observe_global::<FileAssociations, _>(|_, cx| {
+                cx.notify();
+            })
+            .detach();
+
             let view_id = cx.view_id();
             let mut this = Self {
                 project: project.clone(),
@@ -1166,7 +1176,10 @@ impl ProjectPanel {
             }
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
-            let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
+            let (git_status_setting, show_file_icons) = {
+                let settings = settings::get::<ProjectPanelSettings>(cx);
+                (settings.git_status, settings.file_icons)
+            };
             if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
                 let snapshot = worktree.read(cx).snapshot();
                 let root_name = OsStr::new(snapshot.root_name());
@@ -1179,6 +1192,11 @@ impl ProjectPanel {
                 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
                 for entry in visible_worktree_entries[entry_range].iter() {
                     let status = git_status_setting.then(|| entry.git_status).flatten();
+                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
+                    let icon = show_file_icons.then(|| match entry.kind {
+                        EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx),
+                        _ => FileAssociations::get_folder_icon(is_expanded, cx),
+                    });
 
                     let mut details = EntryDetails {
                         filename: entry
@@ -1187,11 +1205,12 @@ impl ProjectPanel {
                             .unwrap_or(root_name)
                             .to_string_lossy()
                             .to_string(),
+                        icon,
                         path: entry.path.clone(),
                         depth: entry.path.components().count(),
                         kind: entry.kind,
                         is_ignored: entry.is_ignored,
-                        is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
+                        is_expanded,
                         is_selected: self.selection.map_or(false, |e| {
                             e.worktree_id == snapshot.id() && e.entry_id == entry.id
                         }),
@@ -1254,23 +1273,36 @@ impl ProjectPanel {
             .unwrap_or(style.text.color);
 
         Flex::row()
-            .with_child(
-                if kind.is_dir() {
-                    if details.is_expanded {
-                        Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
-                    } else {
-                        Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
-                    }
+            .with_child(if let Some(icon) = &details.icon {
+                Svg::new(icon.to_string())
+                    .with_color(style.icon_color)
+                    .constrained()
+                    .with_max_width(style.icon_size)
+                    .with_max_height(style.icon_size)
+                    .aligned()
                     .constrained()
+                    .with_width(style.icon_size)
+            } else if kind.is_dir() {
+                if details.is_expanded {
+                    Svg::new("icons/chevron_down_8.svg").with_color(style.chevron_color)
                 } else {
-                    Empty::new().constrained()
+                    Svg::new("icons/chevron_right_8.svg").with_color(style.chevron_color)
                 }
-                .with_max_width(style.icon_size)
-                .with_max_height(style.icon_size)
+                .constrained()
+                .with_max_width(style.chevron_size)
+                .with_max_height(style.chevron_size)
                 .aligned()
                 .constrained()
-                .with_width(style.icon_size),
-            )
+                .with_width(style.chevron_size)
+            } else {
+                Empty::new()
+                    .constrained()
+                    .with_max_width(style.chevron_size)
+                    .with_max_height(style.chevron_size)
+                    .aligned()
+                    .constrained()
+                    .with_width(style.chevron_size)
+            })
             .with_child(if show_editor && editor.is_some() {
                 ChildView::new(editor.as_ref().unwrap(), cx)
                     .contained()
@@ -2581,7 +2613,7 @@ mod tests {
             theme::init((), cx);
             language::init(cx);
             editor::init_settings(cx);
-            crate::init(cx);
+            crate::init((), cx);
             workspace::init_settings(cx);
             Project::init_settings(cx);
         });
@@ -2596,7 +2628,7 @@ mod tests {
             language::init(cx);
             editor::init(cx);
             pane::init(cx);
-            crate::init(cx);
+            crate::init((), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);
         });

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -13,6 +13,7 @@ pub enum ProjectPanelDockPosition {
 #[derive(Deserialize, Debug)]
 pub struct ProjectPanelSettings {
     pub git_status: bool,
+    pub file_icons: bool,
     pub dock: ProjectPanelDockPosition,
     pub default_width: f32,
 }
@@ -20,6 +21,7 @@ pub struct ProjectPanelSettings {
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct ProjectPanelSettingsContent {
     pub git_status: Option<bool>,
+    pub file_icons: Option<bool>,
     pub dock: Option<ProjectPanelDockPosition>,
     pub default_width: Option<f32>,
 }

crates/theme/src/theme.rs 🔗

@@ -480,8 +480,10 @@ pub struct ProjectPanelEntry {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub text: TextStyle,
-    pub icon_color: Color,
     pub icon_size: f32,
+    pub icon_color: Color,
+    pub chevron_color: Color,
+    pub chevron_size: f32,
     pub icon_spacing: f32,
     pub status: EntryStatus,
 }

crates/zed/src/main.rs 🔗

@@ -154,7 +154,7 @@ fn main() {
         file_finder::init(cx);
         outline::init(cx);
         project_symbols::init(cx);
-        project_panel::init(cx);
+        project_panel::init(Assets, cx);
         diagnostics::init(cx);
         search::init(cx);
         vector_store::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -166,6 +166,7 @@ fn main() {
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
         cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
             .detach();
+        watch_file_types(fs.clone(), cx);
 
         languages.set_theme(theme::current(cx).clone());
         cx.observe_global::<SettingsStore, _>({
@@ -685,6 +686,26 @@ async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> O
     Some(())
 }
 
+#[cfg(debug_assertions)]
+fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    cx.spawn(|mut cx| async move {
+        let mut events = fs
+            .watch(
+                "assets/icons/file_icons/file_types.json".as_ref(),
+                Duration::from_millis(100),
+            )
+            .await;
+        while (events.next().await).is_some() {
+            cx.update(|cx| {
+                cx.update_global(|file_types, _| {
+                    *file_types = project_panel::file_associations::FileAssociations::new(Assets);
+                });
+            })
+        }
+    })
+    .detach()
+}
+
 #[cfg(not(debug_assertions))]
 async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
     None
@@ -695,6 +716,11 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
     None
 }
 
+#[cfg(not(debug_assertions))]
+fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    None
+}
+
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {

crates/zed/src/zed.rs 🔗

@@ -2334,7 +2334,7 @@ mod tests {
             editor::init(cx);
             project_panel::init_settings(cx);
             pane::init(cx);
-            project_panel::init(cx);
+            project_panel::init((), cx);
             terminal_view::init(cx);
             ai::init(cx);
             app_state

script/generate-licenses 🔗

@@ -26,4 +26,4 @@ sed -i '' 's/&#x27;/'\''/g' $OUTPUT_FILE # The ` '\'' ` thing ends the string, a
 sed -i '' 's/&#x3D;/=/g' $OUTPUT_FILE
 sed -i '' 's/&#x60;/`/g' $OUTPUT_FILE
 sed -i '' 's/&lt;/</g' $OUTPUT_FILE
-sed -i '' 's/&gt;/>/g' $OUTPUT_FILE
+sed -i '' 's/&gt;/>/g' $OUTPUT_FILE

styles/src/style_tree/project_panel.ts 🔗

@@ -46,9 +46,11 @@ export default function project_panel(): any {
         const base_properties = {
             height: 22,
             background: background(theme.middle),
-            icon_color: foreground(theme.middle, "variant"),
-            icon_size: 7,
-            icon_spacing: 5,
+            chevron_color: foreground(theme.middle, "variant"),
+            icon_color: with_opacity(foreground(theme.middle, "active"), 0.3),
+            chevron_size: 7,
+            icon_size: 14,
+            icon_spacing: 6,
             text: text(theme.middle, "sans", "variant", { size: "sm" }),
             status: {
                 ...git_status,
@@ -62,17 +64,17 @@ export default function project_panel(): any {
         const unselected_default_style = merge(
             base_properties,
             unselected?.default ?? {},
-            {}
+            {},
         )
         const unselected_hovered_style = merge(
             base_properties,
             { background: background(theme.middle, "hovered") },
-            unselected?.hovered ?? {}
+            unselected?.hovered ?? {},
         )
         const unselected_clicked_style = merge(
             base_properties,
             { background: background(theme.middle, "pressed") },
-            unselected?.clicked ?? {}
+            unselected?.clicked ?? {},
         )
         const selected_default_style = merge(
             base_properties,
@@ -80,7 +82,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.default ?? {}
+            selected_style?.default ?? {},
         )
         const selected_hovered_style = merge(
             base_properties,
@@ -88,7 +90,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "hovered"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.hovered ?? {}
+            selected_style?.hovered ?? {},
         )
         const selected_clicked_style = merge(
             base_properties,
@@ -96,7 +98,7 @@ export default function project_panel(): any {
                 background: background(theme.lowest, "pressed"),
                 text: text(theme.lowest, "sans", { size: "sm" }),
             },
-            selected_style?.clicked ?? {}
+            selected_style?.clicked ?? {},
         )
 
         return toggleable({
@@ -155,7 +157,7 @@ export default function project_panel(): any {
         }),
         background: background(theme.middle),
         padding: { left: 6, right: 6, top: 0, bottom: 6 },
-        indent_width: 12,
+        indent_width: 20,
         entry: default_entry,
         dragged_entry: {
             ...default_entry.inactive.default,
@@ -173,7 +175,7 @@ export default function project_panel(): any {
                 default: {
                     icon_color: foreground(theme.middle, "variant"),
                 },
-            }
+            },
         ),
         cut_entry: entry(
             {
@@ -188,7 +190,7 @@ export default function project_panel(): any {
                         size: "sm",
                     }),
                 },
-            }
+            },
         ),
         filename_editor: {
             background: background(theme.middle, "on"),