Compare commits
6 Commits
gpu-prefer
...
gemini
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9445013dd6 | ||
|
|
74cf3d2d92 | ||
|
|
427491a24f | ||
|
|
2781b1cce1 | ||
|
|
ab69c05d99 | ||
|
|
f06c3b5670 |
@@ -19,10 +19,6 @@
|
||||
# https://github.com/zed-industries/zed/pull/2394
|
||||
eca93c124a488b4e538946cd2d313bd571aa2b86
|
||||
|
||||
# 2024-02-25 Format JSON files in assets/
|
||||
# https://github.com/zed-industries/zed/pull/8405
|
||||
ffdda588b41f7d9d270ffe76cab116f828ad545e
|
||||
|
||||
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
|
||||
# https://github.com/zed-industries/zed/pull/13887
|
||||
813cc3f5e537372fc86720b5e71b6e1c815440ab
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
3
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -27,8 +27,7 @@ body:
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
macOS: `~/Library/Logs/Zed/Zed.log`
|
||||
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
|
||||
57
.github/workflows/release_nightly.yml
vendored
57
.github/workflows/release_nightly.yml
vendored
@@ -93,9 +93,9 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-linux-x86:
|
||||
bundle-deb:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux *.tar.gz bundle for x86
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -121,57 +121,8 @@ jobs:
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-linux-arm:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux *.tar.gz bundle for ARM
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- hosted-linux-arm-1
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
- name: rustup
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
78
Cargo.lock
generated
78
Cargo.lock
generated
@@ -341,9 +341,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
|
||||
dependencies = [
|
||||
"async-fs 2.1.1",
|
||||
"async-net 2.0.0",
|
||||
@@ -2564,7 +2563,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"call",
|
||||
"channel",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
@@ -3403,13 +3401,11 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
@@ -3594,7 +3590,6 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
@@ -3603,7 +3598,6 @@ dependencies = [
|
||||
"db",
|
||||
"emojis",
|
||||
"env_logger",
|
||||
"file_icons",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"git",
|
||||
@@ -3990,7 +3984,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
@@ -4010,7 +4003,6 @@ dependencies = [
|
||||
"theme_selector",
|
||||
"ui",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -4827,6 +4819,7 @@ name = "google_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures 0.3.28",
|
||||
"http 0.1.0",
|
||||
"serde",
|
||||
@@ -4909,7 +4902,6 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"oo7",
|
||||
"open",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
@@ -5072,7 +5064,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"extension",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
@@ -5496,6 +5487,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"project",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -5721,25 +5713,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isahc"
|
||||
version = "1.7.2"
|
||||
@@ -7222,17 +7195,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open_ai"
|
||||
version = "0.1.0"
|
||||
@@ -8080,7 +8042,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"shellexpand 2.1.2",
|
||||
"shlex",
|
||||
"similar",
|
||||
"smol",
|
||||
@@ -8318,9 +8279,7 @@ dependencies = [
|
||||
"search",
|
||||
"settings",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8527,6 +8486,7 @@ dependencies = [
|
||||
"client",
|
||||
"dev_server_projects",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -8538,7 +8498,6 @@ dependencies = [
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal_view",
|
||||
@@ -10922,18 +10881,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.62"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.62"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -13493,7 +13452,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "xkbcommon"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=fcbb4612185cc129ceeff51d22f7fb51810a03b2#fcbb4612185cc129ceeff51d22f7fb51810a03b2"
|
||||
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=2d4c4439160c7846ede0f0ece93bf73b1e613339#2d4c4439160c7846ede0f0ece93bf73b1e613339"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"libc",
|
||||
@@ -13630,7 +13589,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.146.0"
|
||||
version = "0.145.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13699,7 +13658,6 @@ dependencies = [
|
||||
"release_channel",
|
||||
"repl",
|
||||
"rope",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13773,7 +13731,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.6"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13873,9 +13831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.1.1"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13894,7 +13852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruby"
|
||||
version = "0.0.8"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13909,7 +13867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_svelte"
|
||||
version = "0.0.3"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13951,9 +13909,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.4"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -274,7 +274,7 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
ashpd = "0.9.1"
|
||||
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
@@ -457,6 +457,7 @@ features = [
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Time",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.49574 4.74787L5.99574 7.25214L8.49574 4.74787" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 246 B |
@@ -1,13 +1,14 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_62_95)">
|
||||
<path d="M4.5 6C3.67157 6 3 5.32843 3 4.5C3 3.67157 3.67157 3 4.5 3C5.32843 3 6 3.67157 6 4.5C6 5.32843 5.32843 6 4.5 6Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.54433 13.4334C6.87775 13.5227 7.22046 13.3249 7.3098 12.9914C7.39914 12.658 7.20127 12.3153 6.86786 12.226L6.54433 13.4334ZM3.77426 6.86772L3.93603 6.26401L2.72862 5.94049L2.56686 6.54419L3.77426 6.86772ZM6.86786 12.226C4.53394 11.6006 3.14889 9.20163 3.77426 6.86772L2.56686 6.54419C1.76281 9.54494 3.54359 12.6293 6.54433 13.4334L6.86786 12.226Z" fill="white"/>
|
||||
<path d="M11.5 13C10.6716 13 10 12.3284 10 11.5C10 10.6716 10.6716 10 11.5 10C12.3284 10 13 10.6716 13 11.5C13 12.3284 12.3284 13 11.5 13Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1875 4.21113C9.88852 4.03854 9.7861 3.65629 9.95869 3.35736C10.1313 3.05843 10.5135 2.95601 10.8125 3.12859L10.1875 4.21113ZM11.7888 10.1875C12.9969 8.09496 12.28 5.41925 10.1875 4.21113L10.8125 3.12859C13.5028 4.6819 14.4246 8.12209 12.8713 10.8125L11.7888 10.1875Z" fill="white"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_58)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_62_95">
|
||||
<rect width="16" height="16" fill="white" transform="matrix(-1 0 0 1 16 0)"/>
|
||||
<clipPath id="clip0_32_58">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -19,6 +19,7 @@
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
@@ -78,8 +79,10 @@
|
||||
"shift-down": "editor::SelectDown",
|
||||
"shift-left": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
@@ -205,7 +208,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace > Editor",
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-alt-enter": "search::ReplaceAll"
|
||||
@@ -258,19 +261,20 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"shift-alt-up": "editor::AddSelectionAbove",
|
||||
"shift-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
@@ -323,7 +327,7 @@
|
||||
"alt-9": ["pane::ActivateItem", 8],
|
||||
"alt-0": "pane::ActivateLastItem",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-alt-shift--": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
@@ -563,13 +567,6 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"ctrl-l": "editor::NextScreen",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -102,9 +102,9 @@
|
||||
"ctrl-shift-b": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-f": "editor::SelectRight",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
@@ -255,7 +255,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace > Editor",
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
@@ -302,20 +302,19 @@
|
||||
"bindings": {
|
||||
"cmd-[": "editor::Outdent",
|
||||
"cmd-]": "editor::Indent",
|
||||
"cmd-alt-up": "editor::AddSelectionAbove", // Insert cursor above
|
||||
"cmd-alt-up": "editor::AddSelectionAbove",
|
||||
"cmd-ctrl-p": "editor::AddSelectionAbove",
|
||||
"cmd-alt-down": "editor::AddSelectionBelow", // Insert cursor below
|
||||
"cmd-alt-down": "editor::AddSelectionBelow",
|
||||
"cmd-ctrl-n": "editor::AddSelectionBelow",
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // Add selection to Next Find Match
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"cmd-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
@@ -595,14 +594,6 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "picker::ConfirmCompletion",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
@@ -622,6 +613,14 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Picker",
|
||||
"bindings": {
|
||||
"f2": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Zed keymap
|
||||
//
|
||||
// For information on binding keys, see the Zed
|
||||
// documentation: https://zed.dev/docs/key-bindings
|
||||
//
|
||||
// To see the default key bindings run `zed: Open Default Keymap`
|
||||
// from the command palette.
|
||||
[
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// "shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
// "j k": ["workspace::SendKeystrokes", "escape"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -25,7 +25,6 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-m": "editor::ScrollCursorCenter",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
"ctrl-pageup": "editor::MovePageUp",
|
||||
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePrevItem"
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -333,10 +333,7 @@
|
||||
"context": "vim_mode == waiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
"enter": "vim::Enter"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -94,9 +94,6 @@
|
||||
// 3. Never close the window
|
||||
// "when_closing_with_no_tabs": "keep_window_open",
|
||||
"when_closing_with_no_tabs": "platform_default",
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// How to highlight the current line in the editor.
|
||||
@@ -434,9 +431,7 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false
|
||||
"close_position": "right"
|
||||
},
|
||||
// Settings related to preview tabs.
|
||||
"preview_tabs": {
|
||||
@@ -790,7 +785,6 @@
|
||||
}
|
||||
},
|
||||
"PHP": {
|
||||
"language_servers": ["phpactor", "!intelephense", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
@@ -798,7 +792,7 @@
|
||||
}
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// documentation: https://zed.dev/docs/configuring-zed
|
||||
//
|
||||
// To see all of Zed's default settings without changing your
|
||||
// custom settings, run the `zed: Open Default Settings` command
|
||||
// from the command palette
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
{
|
||||
"ui_font_size": 16,
|
||||
"buffer_font_size": 16,
|
||||
|
||||
@@ -30,8 +30,8 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, diagnostics_command, docs_command, fetch_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, symbols_command,
|
||||
tabs_command, term_command,
|
||||
file_command, now_command, project_command, prompt_command, search_command, tabs_command,
|
||||
term_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
@@ -189,8 +189,12 @@ pub struct LanguageModelRequest {
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: f32,
|
||||
pub cached_contents: Vec<CachedContentId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct CachedContentId(String);
|
||||
|
||||
impl LanguageModelRequest {
|
||||
pub fn to_proto(&self) -> proto::CompleteWithLanguageModel {
|
||||
proto::CompleteWithLanguageModel {
|
||||
@@ -200,6 +204,7 @@ impl LanguageModelRequest {
|
||||
temperature: self.temperature,
|
||||
tool_choice: None,
|
||||
tools: Vec::new(),
|
||||
cached_contents: self.cached_contents.iter().map(|id| id.0.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +372,6 @@ fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(symbols_command::OutlineSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::{
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use client::proto;
|
||||
use collections::{BTreeSet, HashMap, HashSet};
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
@@ -59,7 +58,7 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
|
||||
item::{BreadcrumbText, Item, ItemHandle},
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
@@ -67,7 +66,6 @@ use workspace::{
|
||||
use workspace::{searchable::SearchableItemHandle, NewFile};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace
|
||||
@@ -376,7 +374,7 @@ impl AssistantPanel {
|
||||
|
||||
fn handle_pane_event(
|
||||
&mut self,
|
||||
pane: View<Pane>,
|
||||
_pane: View<Pane>,
|
||||
event: &pane::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -386,25 +384,14 @@ impl AssistantPanel {
|
||||
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
|
||||
|
||||
pane::Event::AddItem { item } => {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
item.added_to_pane(workspace, self.pane.clone(), cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pane::Event::ActivateItem { local } => {
|
||||
if *local {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.unfollow_in_pane(&pane, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
}
|
||||
|
||||
pane::Event::RemoveItem { .. } => {
|
||||
pane::Event::RemoveItem { .. } | pane::Event::ActivateItem { .. } => {
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
}
|
||||
|
||||
@@ -595,8 +582,7 @@ impl AssistantPanel {
|
||||
let mut editor = ContextEditor::for_context(
|
||||
context,
|
||||
self.fs.clone(),
|
||||
workspace.clone(),
|
||||
self.project.clone(),
|
||||
workspace,
|
||||
lsp_adapter_delegate,
|
||||
cx,
|
||||
);
|
||||
@@ -627,13 +613,12 @@ impl AssistantPanel {
|
||||
fn handle_context_editor_event(
|
||||
&mut self,
|
||||
_: View<ContextEditor>,
|
||||
event: &EditorEvent,
|
||||
event: &ContextEditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::TitleChanged { .. } => cx.notify(),
|
||||
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
_ => {}
|
||||
ContextEditorEvent::TabContentChanged => cx.notify(),
|
||||
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,7 +693,6 @@ impl AssistantPanel {
|
||||
.context_store
|
||||
.update(cx, |store, cx| store.open_local_context(path.clone(), cx));
|
||||
let fs = self.fs.clone();
|
||||
let project = self.project.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let lsp_adapter_delegate = workspace
|
||||
@@ -725,14 +709,7 @@ impl AssistantPanel {
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace dropped"))?;
|
||||
let editor = cx.new_view(|cx| {
|
||||
ContextEditor::for_context(
|
||||
context,
|
||||
fs,
|
||||
workspace,
|
||||
project,
|
||||
lsp_adapter_delegate,
|
||||
cx,
|
||||
)
|
||||
ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
|
||||
});
|
||||
this.show_context(editor, cx);
|
||||
anyhow::Ok(())
|
||||
@@ -745,17 +722,14 @@ impl AssistantPanel {
|
||||
&mut self,
|
||||
id: ContextId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<View<ContextEditor>>> {
|
||||
) -> Task<Result<()>> {
|
||||
let existing_context = self.pane.read(cx).items().find_map(|item| {
|
||||
item.downcast::<ContextEditor>()
|
||||
.filter(|editor| *editor.read(cx).context.read(cx).id() == id)
|
||||
});
|
||||
if let Some(existing_context) = existing_context {
|
||||
return cx.spawn(|this, mut cx| async move {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.show_context(existing_context.clone(), cx)
|
||||
})?;
|
||||
Ok(existing_context)
|
||||
this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -779,18 +753,12 @@ impl AssistantPanel {
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace dropped"))?;
|
||||
let editor = cx.new_view(|cx| {
|
||||
ContextEditor::for_context(
|
||||
context,
|
||||
fs,
|
||||
workspace,
|
||||
this.project.clone(),
|
||||
lsp_adapter_delegate,
|
||||
cx,
|
||||
)
|
||||
ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
|
||||
});
|
||||
this.show_context(editor.clone(), cx);
|
||||
anyhow::Ok(editor)
|
||||
})?
|
||||
this.show_context(editor, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -910,14 +878,6 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn pane(&self) -> Option<View<Pane>> {
|
||||
Some(self.pane.clone())
|
||||
}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::AssistantPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
if !settings.enabled || !settings.button {
|
||||
@@ -964,7 +924,6 @@ pub struct ContextEditor {
|
||||
editor: View<Editor>,
|
||||
blocks: HashSet<BlockId>,
|
||||
scroll_position: Option<ScrollPosition>,
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -977,7 +936,6 @@ impl ContextEditor {
|
||||
context: Model<Context>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: View<Workspace>,
|
||||
project: Model<Project>,
|
||||
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
@@ -996,7 +954,6 @@ impl ContextEditor {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Box::new(completion_provider));
|
||||
editor.set_collaboration_hub(Box::new(project));
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -1014,7 +971,6 @@ impl ContextEditor {
|
||||
lsp_adapter_delegate,
|
||||
blocks: Default::default(),
|
||||
scroll_position: None,
|
||||
remote_id: None,
|
||||
fs,
|
||||
workspace: workspace.downgrade(),
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
@@ -1257,7 +1213,7 @@ impl ContextEditor {
|
||||
});
|
||||
}
|
||||
ContextEvent::SummaryChanged => {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
cx.emit(ContextEditorEvent::TabContentChanged);
|
||||
self.context.update(cx, |context, cx| {
|
||||
context.save(None, self.fs.clone(), cx);
|
||||
});
|
||||
@@ -1516,9 +1472,9 @@ impl ContextEditor {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
self.scroll_position = self.cursor_scroll_position(cx);
|
||||
}
|
||||
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
|
||||
_ => {}
|
||||
}
|
||||
cx.emit(event.clone());
|
||||
}
|
||||
|
||||
fn handle_editor_search_event(
|
||||
@@ -1685,11 +1641,9 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
|
||||
let editor = editor.read(cx);
|
||||
let range = editor.selections.newest::<usize>(cx).range();
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
|
||||
..editor::ToOffset::to_offset(&selection.end, &buffer);
|
||||
let start_language = buffer.language_at(range.start);
|
||||
let end_language = buffer.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
@@ -1981,7 +1935,7 @@ impl ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ContextEditor {}
|
||||
impl EventEmitter<ContextEditorEvent> for ContextEditor {}
|
||||
impl EventEmitter<SearchEvent> for ContextEditor {}
|
||||
|
||||
impl Render for ContextEditor {
|
||||
@@ -2023,22 +1977,35 @@ impl FocusableView for ContextEditor {
|
||||
}
|
||||
|
||||
impl Item for ContextEditor {
|
||||
type Event = editor::EditorEvent;
|
||||
type Event = ContextEditorEvent;
|
||||
|
||||
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
|
||||
Some(util::truncate_and_trailoff(&self.title(cx), Self::MAX_TAB_TITLE_LEN).into())
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
cx: &WindowContext,
|
||||
) -> AnyElement {
|
||||
let color = if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
Label::new(util::truncate_and_trailoff(
|
||||
&self.title(cx),
|
||||
Self::MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(color)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
f(item::ItemEvent::Edit);
|
||||
f(item::ItemEvent::UpdateBreadcrumbs);
|
||||
ContextEditorEvent::Edited => {
|
||||
f(workspace::item::ItemEvent::Edit);
|
||||
f(workspace::item::ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
EditorEvent::TitleChanged => {
|
||||
f(item::ItemEvent::UpdateTab);
|
||||
ContextEditorEvent::TabContentChanged => {
|
||||
f(workspace::item::ItemEvent::UpdateTab);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2054,7 +2021,7 @@ impl Item for ContextEditor {
|
||||
&self,
|
||||
theme: &theme::Theme,
|
||||
cx: &AppContext,
|
||||
) -> Option<Vec<item::BreadcrumbText>> {
|
||||
) -> Option<Vec<workspace::item::BreadcrumbText>> {
|
||||
let editor = self.editor.read(cx);
|
||||
let cursor = editor.selections.newest_anchor().head();
|
||||
let multibuffer = &editor.buffer().read(cx);
|
||||
@@ -2166,127 +2133,6 @@ impl SearchableItem for ContextEditor {
|
||||
}
|
||||
}
|
||||
|
||||
impl FollowableItem for ContextEditor {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let context = self.context.read(cx);
|
||||
Some(proto::view::Variant::ContextEditor(
|
||||
proto::view::ContextEditor {
|
||||
context_id: context.id().to_proto(),
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
workspace: View<Workspace>,
|
||||
id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<View<Self>>>> {
|
||||
let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let context_id = ContextId::from_proto(state.context_id);
|
||||
let editor_state = state.editor?;
|
||||
|
||||
let (project, panel) = workspace.update(cx, |workspace, cx| {
|
||||
Some((
|
||||
workspace.project().clone(),
|
||||
workspace.panel::<AssistantPanel>(cx)?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let context_editor =
|
||||
panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let context_editor = context_editor.await?;
|
||||
context_editor
|
||||
.update(&mut cx, |context_editor, cx| {
|
||||
context_editor.remote_id = Some(id);
|
||||
context_editor.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: editor_state.selections,
|
||||
pending_selection: editor_state.pending_selection,
|
||||
scroll_top_anchor: editor_state.scroll_top_anchor,
|
||||
scroll_x: editor_state.scroll_y,
|
||||
scroll_y: editor_state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
Ok(context_editor)
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::Event,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<proto::PeerId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
|
||||
if existing.context.read(cx).id() == self.context.read(cx).id() {
|
||||
Some(item::Dedup::KeepExisting)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextEditorToolbarItem {
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakView<Workspace>,
|
||||
@@ -2523,8 +2369,17 @@ impl EventEmitter<()> for ContextHistory {}
|
||||
impl Item for ContextHistory {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("History".into())
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
_: &WindowContext,
|
||||
) -> AnyElement {
|
||||
let color = if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
Label::new("History").color(color).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2536,13 +2391,10 @@ fn render_slash_command_output_toggle(
|
||||
fold: ToggleFold,
|
||||
_cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
Disclosure::new(
|
||||
("slash-command-output-fold-indicator", row.0 as u64),
|
||||
!is_folded,
|
||||
)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pending_slash_command_gutter_decoration(
|
||||
|
||||
@@ -152,6 +152,11 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
cached_contents: request
|
||||
.cached_contents
|
||||
.iter()
|
||||
.map(|id| id.0.clone())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
self.client
|
||||
|
||||
@@ -1258,6 +1258,7 @@ impl Context {
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!("support context caching")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1513,6 +1514,7 @@ impl Context {
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!
|
||||
};
|
||||
|
||||
let stream = CompletionProvider::global(cx).complete(request, cx);
|
||||
|
||||
@@ -851,6 +851,7 @@ impl InlineAssistant {
|
||||
messages,
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature,
|
||||
cached_contents: Vec::new(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
.flex_none()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.mx_1()
|
||||
.mx_2()
|
||||
.child(editor.clone())
|
||||
}
|
||||
}
|
||||
@@ -744,6 +744,7 @@ impl PromptLibrary {
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
cached_contents: Vec::new(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -766,7 +767,6 @@ impl PromptLibrary {
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.h_full()
|
||||
.px_1()
|
||||
.w_1_3()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
|
||||
@@ -27,7 +27,6 @@ pub mod now_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod search_command;
|
||||
pub mod symbols_command;
|
||||
pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ impl SlashCommand for NowSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Current Date and Time".into()
|
||||
"Insert current date and time".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{IconName, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OutlineSlashCommand;
|
||||
|
||||
impl SlashCommand for OutlineSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"symbols".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Symbols for Active Tab".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return Task::ready(Err(anyhow!("no active tab")));
|
||||
};
|
||||
let Some(buffer) = active_item
|
||||
.downcast::<Editor>()
|
||||
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("active tab is not an editor")));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let outline = snapshot
|
||||
.outline(None)
|
||||
.context("no symbols for active tab")?;
|
||||
|
||||
let path = path.as_deref().unwrap_or(Path::new("untitled"));
|
||||
let mut outline_text = format!("Symbols for {}:\n", path.display());
|
||||
for item in &outline.path_candidates {
|
||||
outline_text.push_str("- ");
|
||||
outline_text.push_str(&item.string);
|
||||
outline_text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..outline_text.len(),
|
||||
icon: IconName::ListTree,
|
||||
label: path.to_string_lossy().to_string().into(),
|
||||
}],
|
||||
text: outline_text,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ impl SlashCommand for TermSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Terminal Output".into()
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -269,6 +269,7 @@ impl TerminalInlineAssistant {
|
||||
messages,
|
||||
stop: Vec::new(),
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
@@ -448,7 +448,12 @@ impl AutoUpdater {
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let binary_path = match OS {
|
||||
// We store the path of our current binary, before we install, since installation might
|
||||
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
|
||||
// So we cache it here, which allows us to then restart later on.
|
||||
let binary_path = cx.update(|cx| cx.app_path())??;
|
||||
|
||||
match OS {
|
||||
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
@@ -531,10 +536,9 @@ async fn install_release_linux(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_tar_gz: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
@@ -565,16 +569,7 @@ async fn install_release_linux(
|
||||
let app_folder_name = format!("zed{}.app", suffix);
|
||||
|
||||
let from = extracted.join(&app_folder_name);
|
||||
let mut to = home_dir.join(".local");
|
||||
|
||||
let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
|
||||
|
||||
if let Some(prefix) = running_app_path
|
||||
.to_str()
|
||||
.and_then(|str| str.strip_suffix(&expected_suffix))
|
||||
{
|
||||
to = PathBuf::from(prefix);
|
||||
}
|
||||
let to = home_dir.join(".local");
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
@@ -591,15 +586,17 @@ async fn install_release_linux(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(to.join(expected_suffix))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_release_macos(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_dmg: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
let running_app_path = cx.update(|cx| cx.app_path())??;
|
||||
) -> Result<()> {
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
@@ -647,5 +644,5 @@ async fn install_release_macos(
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(running_app_path)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -113,30 +113,29 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
|
||||
let Some(item) = active_pane_item else {
|
||||
return ToolbarItemLocation::Hidden;
|
||||
};
|
||||
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
if let Some(item) = active_pane_item {
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct IpcHandshake {
|
||||
pub enum CliRequest {
|
||||
Open {
|
||||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
dev_server_token: Option<String>,
|
||||
|
||||
@@ -5,7 +5,6 @@ use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
@@ -38,7 +37,8 @@ struct Args {
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
||||
paths_with_position: Vec<String>,
|
||||
#[arg(value_parser = parse_path_with_position)]
|
||||
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
version: bool,
|
||||
@@ -53,30 +53,12 @@ struct Args {
|
||||
dev_server_token: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(argument_str: &str) -> Result<String, std::io::Error> {
|
||||
let path_like = PathLikeWithPosition::parse_str::<Infallible>(argument_str, |_, path_str| {
|
||||
fn parse_path_with_position(
|
||||
argument_str: &str,
|
||||
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
|
||||
PathLikeWithPosition::parse_str(argument_str, |_, path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
.unwrap();
|
||||
let curdir = env::current_dir()?;
|
||||
|
||||
let canonicalized = path_like.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -109,6 +91,28 @@ fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let curdir = env::current_dir()?;
|
||||
let mut paths = vec![];
|
||||
for path in args.paths_with_position {
|
||||
let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => {
|
||||
if let Some(mut parent) = path.parent() {
|
||||
if parent == Path::new("") {
|
||||
parent = &curdir;
|
||||
}
|
||||
match fs::canonicalize(parent) {
|
||||
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
paths.push(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
@@ -122,19 +126,6 @@ fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
let mut urls = vec![];
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
|| path.starts_with("https://")
|
||||
|| path.starts_with("file://")
|
||||
{
|
||||
urls.push(path.to_string());
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
|
||||
let exit_status = exit_status.clone();
|
||||
@@ -143,7 +134,6 @@ fn main() -> Result<()> {
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
|
||||
@@ -414,5 +414,5 @@ CREATE TABLE dev_servers (
|
||||
CREATE TABLE dev_server_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||
paths TEXT NOT NULL
|
||||
path TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
ALTER TABLE dev_server_projects ADD COLUMN paths JSONB NULL;
|
||||
UPDATE dev_server_projects SET paths = to_json(ARRAY[path]);
|
||||
ALTER TABLE dev_server_projects ALTER COLUMN paths SET NOT NULL;
|
||||
ALTER TABLE dev_server_projects ALTER COLUMN path DROP NOT NULL;
|
||||
@@ -101,6 +101,7 @@ pub fn language_model_request_to_google_ai(
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
generation_config: None,
|
||||
safety_settings: None,
|
||||
cached_content: request.cached_contents.into_iter().next(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,6 +126,60 @@ pub fn language_model_request_message_to_google_ai(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cache_language_model_content_request_to_google_ai(
|
||||
request: proto::CacheLanguageModelContent,
|
||||
) -> Result<google_ai::CreateCachedContentRequest> {
|
||||
Ok(google_ai::CreateCachedContentRequest {
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(language_model_request_message_to_google_ai)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
tools: if request.tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
request
|
||||
.tools
|
||||
.into_iter()
|
||||
.try_fold(Vec::new(), |mut acc, tool| {
|
||||
if let Some(variant) = tool.variant {
|
||||
match variant {
|
||||
proto::chat_completion_tool::Variant::Function(f) => {
|
||||
let description = f.description.ok_or_else(|| {
|
||||
anyhow!("Function tool is missing a description")
|
||||
})?;
|
||||
let parameters = f.parameters.ok_or_else(|| {
|
||||
anyhow!("Function tool is missing parameters")
|
||||
})?;
|
||||
let parsed_parameters = serde_json::from_str(¶meters)
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to parse parameters: {}", e)
|
||||
})?;
|
||||
acc.push(google_ai::Tool {
|
||||
function_declarations: vec![
|
||||
google_ai::FunctionDeclaration {
|
||||
name: f.name,
|
||||
description,
|
||||
parameters: parsed_parameters,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(acc)
|
||||
})?,
|
||||
)
|
||||
},
|
||||
ttl: request.ttl_seconds.map(|s| format!("{}s", s)),
|
||||
display_name: None,
|
||||
model: request.model,
|
||||
system_instruction: None,
|
||||
tool_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_tokens_request_to_google_ai(
|
||||
request: proto::CountTokensWithLanguageModel,
|
||||
) -> Result<google_ai::CountTokensRequest> {
|
||||
|
||||
@@ -43,36 +43,11 @@ async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let mut extensions = app
|
||||
let extensions = app
|
||||
.db
|
||||
.get_extensions(params.filter.as_deref(), params.max_schema_version, 500)
|
||||
.await?;
|
||||
|
||||
if let Some(filter) = params.filter.as_deref() {
|
||||
let extension_id = filter.to_lowercase();
|
||||
let mut exact_match = None;
|
||||
extensions.retain(|extension| {
|
||||
if extension.id.as_ref() == &extension_id {
|
||||
exact_match = Some(extension.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
if exact_match.is_none() {
|
||||
exact_match = app
|
||||
.db
|
||||
.get_extensions_by_ids(&[&extension_id], None)
|
||||
.await?
|
||||
.first()
|
||||
.cloned();
|
||||
}
|
||||
|
||||
if let Some(exact_match) = exact_match {
|
||||
extensions.insert(0, exact_match);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(query) = params.filter.as_deref() {
|
||||
let count = extensions.len();
|
||||
tracing::info!(query, count, "extension_search")
|
||||
|
||||
@@ -5,7 +5,7 @@ use rpc::{
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||
IntoActiveModel, ModelTrait, QueryFilter,
|
||||
ModelTrait, QueryFilter,
|
||||
};
|
||||
|
||||
use crate::db::ProjectId;
|
||||
@@ -56,7 +56,12 @@ impl Database {
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -129,7 +134,7 @@ impl Database {
|
||||
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
||||
paths: ActiveValue::Set(dev_server_project::JSONPaths(vec![path.to_string()])),
|
||||
path: ActiveValue::Set(path.to_string()),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
@@ -143,38 +148,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_dev_server_project(
|
||||
&self,
|
||||
id: DevServerProjectId,
|
||||
paths: &Vec<String>,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let paths = paths.clone();
|
||||
let Some((project, Some(dev_server))) = dev_server_project::Entity::find_by_id(id)
|
||||
.find_also_related(dev_server::Entity)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
else {
|
||||
return Err(anyhow!("no such dev server project"))?;
|
||||
};
|
||||
|
||||
if dev_server.user_id != user_id {
|
||||
return Err(anyhow!("not your dev server"))?;
|
||||
}
|
||||
let mut project = project.into_active_model();
|
||||
project.paths = ActiveValue::Set(dev_server_project::JSONPaths(paths));
|
||||
let project = project.update(&*tx).await?;
|
||||
|
||||
let status = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok((project, status))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
@@ -285,6 +258,7 @@ impl Database {
|
||||
dev_server_id: DevServerId,
|
||||
connection: ConnectionId,
|
||||
) -> crate::Result<Vec<ResharedProject>> {
|
||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||
self.transaction(|tx| async move {
|
||||
let mut ret = Vec::new();
|
||||
for reshared_project in reshared_projects {
|
||||
@@ -348,6 +322,7 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection_id: ConnectionId,
|
||||
) -> crate::Result<Vec<RejoinedProject>> {
|
||||
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
|
||||
self.transaction(|tx| async move {
|
||||
let mut ret = Vec::new();
|
||||
for rejoined_project in rejoined_projects {
|
||||
|
||||
@@ -19,28 +19,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dev_server_for_user(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<dev_server::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
let server = dev_server::Entity::find_by_id(dev_server_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("no dev server with id {}", dev_server_id))?;
|
||||
if server.user_id != user_id {
|
||||
return Err(anyhow::anyhow!(
|
||||
"dev server {} is not owned by user {}",
|
||||
dev_server_id,
|
||||
user_id
|
||||
))?;
|
||||
}
|
||||
Ok(server)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(dev_server::Entity::find()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::project;
|
||||
use crate::db::{DevServerId, DevServerProjectId};
|
||||
use rpc::proto;
|
||||
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "dev_server_projects")]
|
||||
@@ -10,12 +9,9 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: DevServerProjectId,
|
||||
pub dev_server_id: DevServerId,
|
||||
pub paths: JSONPaths,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
|
||||
pub struct JSONPaths(pub Vec<String>);
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -48,12 +44,7 @@ impl Model {
|
||||
id: self.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: self.dev_server_id.to_proto(),
|
||||
path: self.paths().get(0).cloned().unwrap_or_default(),
|
||||
paths: self.paths().clone(),
|
||||
path: self.path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> &Vec<String> {
|
||||
&self.paths.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,13 +431,11 @@ impl Server {
|
||||
.add_request_handler(user_handler(join_hosted_project))
|
||||
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
||||
.add_request_handler(user_handler(create_dev_server_project))
|
||||
.add_request_handler(user_handler(update_dev_server_project))
|
||||
.add_request_handler(user_handler(delete_dev_server_project))
|
||||
.add_request_handler(user_handler(create_dev_server))
|
||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||
.add_request_handler(user_handler(rename_dev_server))
|
||||
.add_request_handler(user_handler(delete_dev_server))
|
||||
.add_request_handler(user_handler(list_remote_directory))
|
||||
.add_request_handler(dev_server_handler(share_dev_server_project))
|
||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||
.add_request_handler(dev_server_handler(reconnect_dev_server))
|
||||
@@ -618,6 +616,17 @@ impl Server {
|
||||
)
|
||||
}
|
||||
})
|
||||
.add_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
user_handler(move |request, response, session| {
|
||||
cache_language_model_content(
|
||||
request,
|
||||
response,
|
||||
session,
|
||||
app_state.config.google_ai_api_key.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.add_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
user_handler(move |request, response, session| {
|
||||
@@ -2315,69 +2324,6 @@ async fn join_hosted_project(
|
||||
join_project_internal(response, session, &mut project, &replica_id)
|
||||
}
|
||||
|
||||
async fn list_remote_directory(
|
||||
request: proto::ListRemoteDirectory,
|
||||
response: Response<proto::ListRemoteDirectory>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id_supporting(dev_server_id, ZedVersion::with_list_directory())?;
|
||||
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server_for_user(dev_server_id, session.user_id())
|
||||
.await?;
|
||||
|
||||
response.send(
|
||||
session
|
||||
.peer
|
||||
.forward_request(session.connection_id, dev_server_connection_id, request)
|
||||
.await?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_dev_server_project(
|
||||
request: proto::UpdateDevServerProject,
|
||||
response: Response<proto::UpdateDevServerProject>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
|
||||
|
||||
let (dev_server_project, update) = session
|
||||
.db()
|
||||
.await
|
||||
.update_dev_server_project(dev_server_project_id, &request.paths, session.user_id())
|
||||
.await?;
|
||||
|
||||
let projects = session
|
||||
.db()
|
||||
.await
|
||||
.get_projects_for_dev_server(dev_server_project.dev_server_id)
|
||||
.await?;
|
||||
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id_supporting(
|
||||
dev_server_project.dev_server_id,
|
||||
ZedVersion::with_list_directory(),
|
||||
)?;
|
||||
|
||||
session.peer.send(
|
||||
dev_server_connection_id,
|
||||
proto::DevServerInstructions { projects },
|
||||
)?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), update, &session).await;
|
||||
|
||||
response.send(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn create_dev_server_project(
|
||||
request: proto::CreateDevServerProject,
|
||||
response: Response<proto::CreateDevServerProject>,
|
||||
@@ -4788,6 +4734,43 @@ async fn complete_with_anthropic(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cache_language_model_content(
|
||||
request: proto::CacheLanguageModelContent,
|
||||
response: Response<proto::CacheLanguageModelContent>,
|
||||
session: UserSession,
|
||||
google_ai_api_key: Option<Arc<str>>,
|
||||
) -> Result<()> {
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
if !request.model.starts_with("gemini") {
|
||||
return Err(anyhow!(
|
||||
"caching content for model: {:?} is not supported",
|
||||
request.model
|
||||
))?;
|
||||
}
|
||||
|
||||
let api_key = google_ai_api_key
|
||||
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
|
||||
|
||||
let cached_content = google_ai::create_cached_content(
|
||||
session.http_client.as_ref(),
|
||||
google_ai::API_URL,
|
||||
&api_key,
|
||||
crate::ai::cache_language_model_content_request_to_google_ai(request)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CacheLanguageModelContentResponse {
|
||||
name: cached_content.name,
|
||||
create_time: cached_content.create_time.timestamp() as u64,
|
||||
update_time: cached_content.update_time.timestamp() as u64,
|
||||
expire_time: cached_content.expire_time.map(|t| t.timestamp() as u64),
|
||||
total_token_count: cached_content.usage_metadata.total_token_count,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CountTokensWithLanguageModelRateLimit;
|
||||
|
||||
impl RateLimit for CountTokensWithLanguageModelRateLimit {
|
||||
|
||||
@@ -38,10 +38,6 @@ impl ZedVersion {
|
||||
pub fn with_save_as() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 134, 0))
|
||||
}
|
||||
|
||||
pub fn with_list_directory() -> ZedVersion {
|
||||
ZedVersion(SemanticVersion::new(0, 145, 0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VersionedMessage {
|
||||
@@ -191,18 +187,6 @@ impl ConnectionPool {
|
||||
self.connected_dev_servers.get(&dev_server_id).copied()
|
||||
}
|
||||
|
||||
pub fn dev_server_connection_id_supporting(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
required: ZedVersion,
|
||||
) -> Result<ConnectionId> {
|
||||
match self.connected_dev_servers.get(&dev_server_id) {
|
||||
Some(cid) if self.connections[cid].zed_version >= required => Ok(*cid),
|
||||
Some(_) => Err(anyhow!(proto::ErrorCode::RemoteUpgradeRequired)),
|
||||
None => Err(anyhow!(proto::ErrorCode::DevServerOffline)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_user_ids(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -66,7 +66,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
@@ -206,7 +206,7 @@ async fn create_dev_server_project(
|
||||
.update(cx, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].paths, vec!["/remote"]);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
|
||||
@@ -135,7 +135,7 @@ async fn test_basic_following(
|
||||
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, only the active view state is replicated to client B.
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
|
||||
cx_c.executor().run_until_parked();
|
||||
@@ -156,7 +156,7 @@ async fn test_basic_following(
|
||||
);
|
||||
assert_eq!(
|
||||
editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![3..3]
|
||||
vec![3..2]
|
||||
);
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -194,7 +194,7 @@ async fn test_basic_following(
|
||||
|
||||
// Client C unfollows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.unfollow(peer_id_a, cx).unwrap();
|
||||
workspace.unfollow(&workspace.active_pane().clone(), cx);
|
||||
});
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
@@ -398,7 +398,7 @@ async fn test_basic_following(
|
||||
|
||||
// After unfollowing, client B stops receiving updates from client A.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.unfollow(peer_id_a, cx).unwrap()
|
||||
workspace.unfollow(&workspace.active_pane().clone(), cx)
|
||||
});
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_item(&editor_a2, cx)
|
||||
|
||||
@@ -1528,7 +1528,7 @@ async fn test_project_reconnect(
|
||||
});
|
||||
let (worktree_a2, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_worktree("/root-1/dir2", true, cx)
|
||||
p.find_or_create_local_worktree("/root-1/dir2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1601,7 +1601,7 @@ async fn test_project_reconnect(
|
||||
});
|
||||
let (worktree_a3, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_worktree("/root-1/dir3", true, cx)
|
||||
p.find_or_create_local_worktree("/root-1/dir3", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1725,7 +1725,7 @@ async fn test_project_reconnect(
|
||||
// While client B is disconnected, add and remove worktrees from client A's project.
|
||||
let (worktree_a4, _) = project_a1
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_worktree("/root-1/dir4", true, cx)
|
||||
p.find_or_create_local_worktree("/root-1/dir4", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4887,7 +4887,7 @@ async fn test_project_search(
|
||||
let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
|
||||
let (worktree_2, _) = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_worktree("/root/dir-2", true, cx)
|
||||
p.find_or_create_local_worktree("/root/dir-2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -581,7 +581,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree(&new_root_path, true, cx)
|
||||
project.find_or_create_local_worktree(&new_root_path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -805,7 +805,9 @@ impl TestClient {
|
||||
) -> (Model<Project>, WorktreeId) {
|
||||
let project = self.build_empty_local_project(cx);
|
||||
let (worktree, _) = project
|
||||
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
|
||||
.update(cx, |p, cx| {
|
||||
p.find_or_create_local_worktree(root_path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
|
||||
@@ -32,7 +32,6 @@ test-support = [
|
||||
anyhow.workspace = true
|
||||
call.workspace = true
|
||||
channel.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -11,20 +11,21 @@ use editor::{
|
||||
EditorEvent,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
|
||||
Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
actions, AnyElement, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter,
|
||||
FocusableView, IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{item::Dedup, notifications::NotificationId};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -32,7 +33,7 @@ use workspace::{
|
||||
actions!(collab, [CopyLink]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
pub struct ChannelView {
|
||||
@@ -82,56 +83,6 @@ impl ChannelView {
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let channel_view = Self::load(channel_id, workspace, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
|
||||
{
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
channel_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
|
||||
channel_view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let workspace = workspace.read(cx);
|
||||
@@ -156,11 +107,49 @@ impl ChannelView {
|
||||
})
|
||||
})?;
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -385,7 +374,7 @@ impl Item for ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
@@ -398,7 +387,13 @@ impl Item for ChannelView {
|
||||
} else {
|
||||
"channel notes (disconnected)".to_string()
|
||||
};
|
||||
Some(label.into())
|
||||
Label::new(label)
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
@@ -483,6 +478,7 @@ impl FollowableItem for ChannelView {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
@@ -495,7 +491,8 @@ impl FollowableItem for ChannelView {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
|
||||
let open =
|
||||
ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
@@ -566,19 +563,6 @@ impl FollowableItem for ChannelView {
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
|
||||
let existing = existing.channel_buffer.read(cx);
|
||||
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
|
||||
if existing.is_connected() {
|
||||
Some(Dedup::KeepExisting)
|
||||
} else {
|
||||
Some(Dedup::ReplaceExisting)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
@@ -111,7 +111,6 @@ impl ChatPanel {
|
||||
this.is_scrolled_to_bottom = !event.is_scrolled;
|
||||
}));
|
||||
|
||||
let local_offset = chrono::Local::now().offset().local_minus_utc();
|
||||
let mut this = Self {
|
||||
fs,
|
||||
client,
|
||||
@@ -121,7 +120,7 @@ impl ChatPanel {
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_editor: input_editor,
|
||||
local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
|
||||
local_timezone: cx.local_timezone(),
|
||||
subscriptions: Vec::new(),
|
||||
is_scrolled_to_bottom: true,
|
||||
active: false,
|
||||
|
||||
@@ -67,6 +67,5 @@ fn notification_window_options(
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
gpu: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +127,11 @@ impl NotificationPanel {
|
||||
},
|
||||
));
|
||||
|
||||
let local_offset = chrono::Local::now().offset().local_minus_utc();
|
||||
let mut this = Self {
|
||||
fs,
|
||||
client,
|
||||
user_store,
|
||||
local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
|
||||
local_timezone: cx.local_timezone(),
|
||||
channel_store: ChannelStore::global(cx),
|
||||
notification_store: NotificationStore::global(cx),
|
||||
notification_list,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Store {
|
||||
pub struct DevServerProject {
|
||||
pub id: DevServerProjectId,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub paths: Vec<SharedString>,
|
||||
pub path: SharedString,
|
||||
pub dev_server_id: DevServerId,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl From<proto::DevServerProject> for DevServerProject {
|
||||
Self {
|
||||
id: DevServerProjectId(project.id),
|
||||
project_id: project.project_id.map(|id| ProjectId(id)),
|
||||
paths: project.paths.into_iter().map(|path| path.into()).collect(),
|
||||
path: project.path.into(),
|
||||
dev_server_id: DevServerId(project.dev_server_id),
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl Store {
|
||||
.filter(|project| project.dev_server_id == id)
|
||||
.cloned()
|
||||
.collect();
|
||||
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||
projects
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl Store {
|
||||
pub fn dev_server_projects(&self) -> Vec<DevServerProject> {
|
||||
let mut projects: Vec<DevServerProject> =
|
||||
self.dev_server_projects.values().cloned().collect();
|
||||
projects.sort_by_key(|p| (p.paths.clone(), p.id));
|
||||
projects.sort_by_key(|p| (p.path.clone(), p.id));
|
||||
projects
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,11 @@ collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -4,7 +4,6 @@ mod toolbar_controls;
|
||||
|
||||
#[cfg(test)]
|
||||
mod diagnostics_tests;
|
||||
mod grouped_diagnostics;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashSet};
|
||||
@@ -15,7 +14,6 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedSender},
|
||||
StreamExt as _,
|
||||
@@ -45,7 +43,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
@@ -54,9 +52,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
ProjectDiagnosticsSettings::register(cx);
|
||||
cx.observe_new_views(ProjectDiagnosticsEditor::register)
|
||||
.detach();
|
||||
if !cx.has_flag::<feature_flags::GroupedDiagnostics>() {
|
||||
grouped_diagnostics::init(cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectDiagnosticsEditor {
|
||||
@@ -471,9 +466,7 @@ impl ProjectDiagnosticsEditor {
|
||||
position: (excerpt_id, entry.range.start),
|
||||
height: diagnostic.message.matches('\n').count() as u8 + 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: diagnostic_block_renderer(
|
||||
diagnostic, None, true, true,
|
||||
),
|
||||
render: diagnostic_block_renderer(diagnostic, true),
|
||||
disposition: BlockDisposition::Below,
|
||||
});
|
||||
}
|
||||
@@ -786,12 +779,26 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("diagnostics")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
|
||||
}
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
|
||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
|
||||
let message: SharedString = message;
|
||||
Box::new(move |cx| {
|
||||
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
|
||||
|
||||
@@ -973,8 +973,8 @@ fn editor_blocks(
|
||||
blocks.extend(
|
||||
snapshot
|
||||
.blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
|
||||
.filter_map(|(row, block)| {
|
||||
let transform_block_id = block.id();
|
||||
.enumerate()
|
||||
.filter_map(|(ix, (row, block))| {
|
||||
let name: SharedString = match block {
|
||||
TransformBlock::Custom(block) => {
|
||||
let mut element = block.render(&mut BlockContext {
|
||||
@@ -984,7 +984,7 @@ fn editor_blocks(
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
transform_block_id,
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
});
|
||||
let element = element.downcast_mut::<Stateful<Div>>().unwrap();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,14 +31,12 @@ test-support = [
|
||||
aho-corasick = "1.1"
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case = "0.6.0"
|
||||
db.workspace = true
|
||||
emojis.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
|
||||
@@ -8,7 +8,6 @@ use gpui::{
|
||||
use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use time::UtcOffset;
|
||||
use ui::{
|
||||
div, h_flex, tooltip_container, v_flex, Avatar, Button, ButtonStyle, Clickable as _, Color,
|
||||
FluentBuilder, Icon, IconName, IconPosition, InteractiveElement as _, IntoElement,
|
||||
@@ -130,7 +129,7 @@ impl Render for BlameEntryTooltip {
|
||||
let author_email = self.blame_entry.author_mail.clone();
|
||||
|
||||
let short_commit_id = self.blame_entry.sha.display_short();
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry);
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
|
||||
|
||||
let message = self
|
||||
.details
|
||||
@@ -248,25 +247,30 @@ impl Render for BlameEntryTooltip {
|
||||
}
|
||||
}
|
||||
|
||||
fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
|
||||
fn blame_entry_timestamp(
|
||||
blame_entry: &BlameEntry,
|
||||
format: time_format::TimestampFormat,
|
||||
cx: &WindowContext,
|
||||
) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
format,
|
||||
)
|
||||
}
|
||||
Ok(timestamp) => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
cx.local_timezone(),
|
||||
format,
|
||||
),
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative)
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
|
||||
}
|
||||
|
||||
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::MediumAbsolute)
|
||||
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(
|
||||
blame_entry,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ use crate::{
|
||||
pub use block_map::{
|
||||
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
|
||||
BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
TransformBlockId,
|
||||
};
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use crate::{EditorStyle, GutterDimensions};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
|
||||
use gpui::{AnyElement, Pixels, WindowContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
|
||||
use parking_lot::Mutex;
|
||||
@@ -20,7 +20,6 @@ use std::{
|
||||
};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use text::Edit;
|
||||
use ui::ElementId;
|
||||
|
||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||
|
||||
@@ -54,12 +53,6 @@ pub struct BlockSnapshot {
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct BlockId(usize);
|
||||
|
||||
impl Into<ElementId> for BlockId {
|
||||
fn into(self) -> ElementId {
|
||||
ElementId::Integer(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct BlockPoint(pub Point);
|
||||
|
||||
@@ -69,7 +62,7 @@ pub struct BlockRow(pub(super) u32);
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Box<dyn Send + FnMut(&mut BlockContext) -> AnyElement>;
|
||||
pub type RenderBlock = Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
@@ -84,22 +77,11 @@ pub struct BlockProperties<P> {
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub style: BlockStyle,
|
||||
pub render: RenderBlock,
|
||||
pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
|
||||
pub disposition: BlockDisposition,
|
||||
}
|
||||
|
||||
impl<P: Debug> Debug for BlockProperties<P> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("BlockProperties")
|
||||
.field("position", &self.position)
|
||||
.field("height", &self.height)
|
||||
.field("style", &self.style)
|
||||
.field("disposition", &self.disposition)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum BlockStyle {
|
||||
Fixed,
|
||||
Flex,
|
||||
@@ -113,47 +95,10 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub gutter_dimensions: &'b GutterDimensions,
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub transform_block_id: TransformBlockId,
|
||||
pub block_id: usize,
|
||||
pub editor_style: &'b EditorStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TransformBlockId {
|
||||
Block(BlockId),
|
||||
ExcerptHeader(ExcerptId),
|
||||
ExcerptFooter(ExcerptId),
|
||||
}
|
||||
|
||||
impl From<TransformBlockId> for EntityId {
|
||||
fn from(value: TransformBlockId) -> Self {
|
||||
match value {
|
||||
TransformBlockId::Block(BlockId(id)) => EntityId::from(id as u64),
|
||||
TransformBlockId::ExcerptHeader(id) => id.into(),
|
||||
TransformBlockId::ExcerptFooter(id) => id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<ElementId> for TransformBlockId {
|
||||
fn into(self) -> ElementId {
|
||||
match self {
|
||||
Self::Block(BlockId(id)) => ("Block", id).into(),
|
||||
Self::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
|
||||
Self::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransformBlockId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Block(id) => write!(f, "Block({id:?})"),
|
||||
Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
|
||||
Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the block should be considered above or below the anchor line
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum BlockDisposition {
|
||||
@@ -212,14 +157,6 @@ impl BlockLike for TransformBlock {
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
pub fn id(&self) -> TransformBlockId {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => TransformBlockId::Block(block.id),
|
||||
TransformBlock::ExcerptHeader { id, .. } => TransformBlockId::ExcerptHeader(*id),
|
||||
TransformBlock::ExcerptFooter { id, .. } => TransformBlockId::ExcerptFooter(*id),
|
||||
}
|
||||
}
|
||||
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
TransformBlock::Custom(block) => block.disposition,
|
||||
|
||||
@@ -164,7 +164,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
new_tree
|
||||
};
|
||||
|
||||
let edits = consolidate_inlay_edits(edits);
|
||||
consolidate_inlay_edits(&mut edits);
|
||||
let edits = self.0.sync(snapshot.clone(), edits);
|
||||
(self.0.snapshot.clone(), edits)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds
|
||||
};
|
||||
|
||||
let edits = consolidate_inlay_edits(edits);
|
||||
consolidate_inlay_edits(&mut edits);
|
||||
let edits = self.0.sync(snapshot.clone(), edits);
|
||||
(self.0.snapshot.clone(), edits)
|
||||
}
|
||||
@@ -513,7 +513,7 @@ impl FoldMap {
|
||||
});
|
||||
}
|
||||
|
||||
fold_edits = consolidate_fold_edits(fold_edits);
|
||||
consolidate_fold_edits(&mut fold_edits);
|
||||
}
|
||||
|
||||
self.snapshot.transforms = new_transforms;
|
||||
@@ -809,7 +809,7 @@ where
|
||||
cursor
|
||||
}
|
||||
|
||||
fn consolidate_inlay_edits(mut edits: Vec<InlayEdit>) -> Vec<InlayEdit> {
|
||||
fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
a.old
|
||||
.start
|
||||
@@ -817,68 +817,42 @@ fn consolidate_inlay_edits(mut edits: Vec<InlayEdit>) -> Vec<InlayEdit> {
|
||||
.then_with(|| b.old.end.cmp(&a.old.end))
|
||||
});
|
||||
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut inlay_edits = edits.into_iter();
|
||||
let inlay_edits = if let Some(mut first_edit) = inlay_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = inlay_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
debug_assert_eq!(_old_alloc_ptr, v.as_ptr(), "Inlay edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
inlay_edits
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn consolidate_fold_edits(mut edits: Vec<FoldEdit>) -> Vec<FoldEdit> {
|
||||
fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
|
||||
edits.sort_unstable_by(|a, b| {
|
||||
a.old
|
||||
.start
|
||||
.cmp(&b.old.start)
|
||||
.then_with(|| b.old.end.cmp(&a.old.end))
|
||||
});
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut fold_edits = edits.into_iter();
|
||||
let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = fold_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
fold_edits
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
|
||||
prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
|
||||
prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -103,33 +103,20 @@ impl TabMap {
|
||||
}
|
||||
}
|
||||
|
||||
let _old_alloc_ptr = fold_edits.as_ptr();
|
||||
// Combine any edits that overlap due to the expansion.
|
||||
let mut fold_edits = fold_edits.into_iter();
|
||||
let fold_edits = if let Some(mut first_edit) = fold_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = fold_edits
|
||||
.scan(&mut first_edit, |state, edit| {
|
||||
if state.old.end >= edit.old.start {
|
||||
state.old.end = edit.old.end;
|
||||
state.new.end = edit.new.end;
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let new_state = edit.clone();
|
||||
let result = Some(Some(state.clone())); // Yield the previous edit
|
||||
**state = new_state;
|
||||
result
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit);
|
||||
debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Fold edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mut ix = 1;
|
||||
while ix < fold_edits.len() {
|
||||
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
|
||||
let prev_edit = prev_edits.last_mut().unwrap();
|
||||
let edit = &next_edits[0];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
fold_edits.remove(ix);
|
||||
} else {
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for fold_edit in fold_edits {
|
||||
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
|
||||
|
||||
@@ -567,7 +567,7 @@ impl WrapSnapshot {
|
||||
});
|
||||
}
|
||||
|
||||
wrap_edits = consolidate_wrap_edits(wrap_edits);
|
||||
consolidate_wrap_edits(&mut wrap_edits);
|
||||
Patch::new(wrap_edits)
|
||||
}
|
||||
|
||||
@@ -1008,33 +1008,19 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
|
||||
}
|
||||
}
|
||||
|
||||
fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
|
||||
let _old_alloc_ptr = edits.as_ptr();
|
||||
let mut wrap_edits = edits.into_iter();
|
||||
let wrap_edits = if let Some(mut first_edit) = wrap_edits.next() {
|
||||
// This code relies on reusing allocations from the Vec<_> - at the time of writing .flatten() prevents them.
|
||||
#[allow(clippy::filter_map_identity)]
|
||||
let mut v: Vec<_> = wrap_edits
|
||||
.scan(&mut first_edit, |prev_edit, edit| {
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
Some(None) // Skip this edit, it's merged
|
||||
} else {
|
||||
let prev = std::mem::replace(*prev_edit, edit);
|
||||
Some(Some(prev)) // Yield the previous edit
|
||||
}
|
||||
})
|
||||
.filter_map(|x| x)
|
||||
.collect();
|
||||
v.push(first_edit.clone());
|
||||
debug_assert_eq!(v.as_ptr(), _old_alloc_ptr, "Wrap edits were reallocated");
|
||||
v
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
wrap_edits
|
||||
fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
|
||||
let mut i = 1;
|
||||
while i < edits.len() {
|
||||
let edit = edits[i].clone();
|
||||
let prev_edit = &mut edits[i - 1];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
edits.remove(i);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -68,12 +68,12 @@ use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText,
|
||||
KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakFocusHandle, WeakView, WhiteSpace, WindowContext,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView,
|
||||
FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
|
||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
|
||||
UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
|
||||
WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -131,7 +131,7 @@ use std::{
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -271,9 +271,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
||||
workspace::register_serializable_item::<Editor>(cx);
|
||||
|
||||
workspace::register_followable_item::<Editor>(cx);
|
||||
workspace::register_deserializable_item::<Editor>(cx);
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(Editor::new_file);
|
||||
@@ -551,7 +550,6 @@ pub struct Editor {
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
git_blame_inline_enabled: bool,
|
||||
serialize_dirty_buffers: bool,
|
||||
show_selection_menu: Option<bool>,
|
||||
blame: Option<Model<GitBlame>>,
|
||||
blame_subscription: Option<Subscription>,
|
||||
@@ -1772,8 +1770,6 @@ impl Editor {
|
||||
);
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, Self::handle_focus).detach();
|
||||
cx.on_focus_in(&focus_handle, Self::handle_focus_in)
|
||||
.detach();
|
||||
cx.on_focus_out(&focus_handle, Self::handle_focus_out)
|
||||
.detach();
|
||||
cx.on_blur(&focus_handle, Self::handle_blur).detach();
|
||||
@@ -1878,9 +1874,6 @@ impl Editor {
|
||||
show_selection_menu: None,
|
||||
show_git_blame_inline_delay_task: None,
|
||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
serialize_dirty_buffers: ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
@@ -9496,11 +9489,6 @@ impl Editor {
|
||||
}
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&rename_editor, |_, _, e, cx| match e {
|
||||
EditorEvent::Focused => cx.emit(EditorEvent::FocusedIn),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let write_highlights =
|
||||
this.clear_background_highlights::<DocumentHighlightWrite>(cx);
|
||||
@@ -9774,7 +9762,7 @@ impl Editor {
|
||||
*block_id,
|
||||
(
|
||||
None,
|
||||
diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid),
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -9827,7 +9815,7 @@ impl Editor {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer.anchor_after(entry.range.start),
|
||||
height: message_height,
|
||||
render: diagnostic_block_renderer(diagnostic, None, true, true),
|
||||
render: diagnostic_block_renderer(diagnostic, true),
|
||||
disposition: BlockDisposition::Below,
|
||||
}
|
||||
}),
|
||||
@@ -10386,22 +10374,6 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn working_directory(&self, cx: &WindowContext) -> Option<PathBuf> {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(dir) = file.abs_path(cx).parent() {
|
||||
return Some(dir.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(project_path) = buffer.read(cx).project_path(cx) {
|
||||
return Some(project_path.path.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
@@ -11271,11 +11243,8 @@ impl Editor {
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
|
||||
let project_settings = ProjectSettings::get_global(cx);
|
||||
self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = project_settings.git.inline_blame_enabled();
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
if self.git_blame_inline_enabled != inline_blame_enabled {
|
||||
self.toggle_git_blame_inline_internal(false, cx);
|
||||
}
|
||||
@@ -11663,10 +11632,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(EditorEvent::FocusedIn)
|
||||
}
|
||||
|
||||
fn handle_focus_out(&mut self, event: FocusOutEvent, _cx: &mut ViewContext<Self>) {
|
||||
if event.blurred != self.focus_handle {
|
||||
self.last_focused_descendant = Some(event.blurred);
|
||||
@@ -12271,7 +12236,6 @@ pub enum EditorEvent {
|
||||
},
|
||||
Reparsed(BufferId),
|
||||
Focused,
|
||||
FocusedIn,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
@@ -12720,17 +12684,11 @@ impl InvalidationRegion for SnippetState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diagnostic_block_renderer(
|
||||
diagnostic: Diagnostic,
|
||||
max_message_rows: Option<u8>,
|
||||
allow_closing: bool,
|
||||
_is_valid: bool,
|
||||
) -> RenderBlock {
|
||||
let (text_without_backticks, code_ranges) =
|
||||
highlight_diagnostic_message(&diagnostic, max_message_rows);
|
||||
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> RenderBlock {
|
||||
let (text_without_backticks, code_ranges) = highlight_diagnostic_message(&diagnostic);
|
||||
|
||||
Box::new(move |cx: &mut BlockContext| {
|
||||
let group_id: SharedString = cx.transform_block_id.to_string().into();
|
||||
let group_id: SharedString = cx.block_id.to_string().into();
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
|
||||
@@ -12742,25 +12700,23 @@ pub fn diagnostic_block_renderer(
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
let buttons = |diagnostic: &Diagnostic, block_id: TransformBlockId| {
|
||||
let buttons = |diagnostic: &Diagnostic, block_id: usize| {
|
||||
if multi_line_diagnostic {
|
||||
v_flex()
|
||||
} else {
|
||||
h_flex()
|
||||
}
|
||||
.when(allow_closing, |div| {
|
||||
div.children(diagnostic.is_primary.then(|| {
|
||||
IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id.clone())
|
||||
.on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
|
||||
.tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
|
||||
}))
|
||||
})
|
||||
.children(diagnostic.is_primary.then(|| {
|
||||
IconButton::new(("close-block", block_id), IconName::XCircle)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id.clone())
|
||||
.on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
|
||||
.tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
|
||||
}))
|
||||
.child(
|
||||
IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
|
||||
IconButton::new(("copy-block", block_id), IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
@@ -12773,12 +12729,12 @@ pub fn diagnostic_block_renderer(
|
||||
)
|
||||
};
|
||||
|
||||
let icon_size = buttons(&diagnostic, cx.transform_block_id)
|
||||
let icon_size = buttons(&diagnostic, cx.block_id)
|
||||
.into_any_element()
|
||||
.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
|
||||
h_flex()
|
||||
.id(cx.transform_block_id)
|
||||
.id(cx.block_id)
|
||||
.group(group_id.clone())
|
||||
.relative()
|
||||
.size_full()
|
||||
@@ -12790,7 +12746,7 @@ pub fn diagnostic_block_renderer(
|
||||
.w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
|
||||
.flex_shrink(),
|
||||
)
|
||||
.child(buttons(&diagnostic, cx.transform_block_id))
|
||||
.child(buttons(&diagnostic, cx.block_id))
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
&text_style,
|
||||
@@ -12809,10 +12765,7 @@ pub fn diagnostic_block_renderer(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
diagnostic: &Diagnostic,
|
||||
mut max_message_rows: Option<u8>,
|
||||
) -> (SharedString, Vec<Range<usize>>) {
|
||||
pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, Vec<Range<usize>>) {
|
||||
let mut text_without_backticks = String::new();
|
||||
let mut code_ranges = Vec::new();
|
||||
|
||||
@@ -12824,45 +12777,18 @@ pub fn highlight_diagnostic_message(
|
||||
|
||||
let mut prev_offset = 0;
|
||||
let mut in_code_block = false;
|
||||
let mut newline_indices = diagnostic
|
||||
.message
|
||||
.match_indices('\n')
|
||||
.map(|(ix, _)| ix)
|
||||
.fuse()
|
||||
.peekable();
|
||||
for (ix, _) in diagnostic
|
||||
.message
|
||||
.match_indices('`')
|
||||
.chain([(diagnostic.message.len(), "")])
|
||||
{
|
||||
let mut trimmed_ix = ix;
|
||||
while let Some(newline_index) = newline_indices.peek() {
|
||||
if *newline_index < ix {
|
||||
if let Some(rows_left) = &mut max_message_rows {
|
||||
if *rows_left == 0 {
|
||||
trimmed_ix = newline_index.saturating_sub(1);
|
||||
break;
|
||||
} else {
|
||||
*rows_left -= 1;
|
||||
}
|
||||
}
|
||||
let _ = newline_indices.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let prev_len = text_without_backticks.len();
|
||||
let new_text = &diagnostic.message[prev_offset..trimmed_ix];
|
||||
text_without_backticks.push_str(new_text);
|
||||
text_without_backticks.push_str(&diagnostic.message[prev_offset..ix]);
|
||||
prev_offset = ix + 1;
|
||||
if in_code_block {
|
||||
code_ranges.push(prev_len..text_without_backticks.len());
|
||||
}
|
||||
prev_offset = trimmed_ix + 1;
|
||||
in_code_block = !in_code_block;
|
||||
if trimmed_ix != ix {
|
||||
text_without_backticks.push_str("...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(text_without_backticks.into(), code_ranges)
|
||||
|
||||
@@ -8812,6 +8812,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
let follower_1 = cx
|
||||
.update_window(*workspace.deref(), |_, cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
workspace.root_view(cx).unwrap(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
@@ -8903,6 +8904,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
let follower_2 = cx
|
||||
.update_window(*workspace.deref(), |_, cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
workspace.root_view(cx).unwrap().clone(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::editor_settings::ScrollBeyondLastLine;
|
||||
use crate::TransformBlockId;
|
||||
use crate::{
|
||||
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
|
||||
display_map::{
|
||||
@@ -32,7 +31,7 @@ use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
EntityId, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
@@ -409,10 +408,8 @@ impl EditorElement {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
if editor.hover_state.focused(cx) {
|
||||
return;
|
||||
}
|
||||
Self::modifiers_changed(editor, event, &position_map, &text_hitbox, cx)
|
||||
})
|
||||
}
|
||||
@@ -1940,6 +1937,7 @@ impl EditorElement {
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<BlockLayout> {
|
||||
let mut block_id = 0;
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
.partition::<Vec<_>, _>(|(_, block)| match block {
|
||||
@@ -1950,7 +1948,7 @@ impl EditorElement {
|
||||
|
||||
let render_block = |block: &TransformBlock,
|
||||
available_space: Size<AvailableSpace>,
|
||||
block_id: TransformBlockId,
|
||||
block_id: usize,
|
||||
block_row_start: DisplayRow,
|
||||
cx: &mut WindowContext| {
|
||||
let mut element = match block {
|
||||
@@ -1974,7 +1972,7 @@ impl EditorElement {
|
||||
gutter_dimensions,
|
||||
line_height,
|
||||
em_width,
|
||||
transform_block_id: block_id,
|
||||
block_id,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
})
|
||||
@@ -2058,7 +2056,7 @@ impl EditorElement {
|
||||
let header_padding = px(6.0);
|
||||
|
||||
v_flex()
|
||||
.id(("path excerpt header", EntityId::from(block_id)))
|
||||
.id(("path excerpt header", block_id))
|
||||
.size_full()
|
||||
.p(header_padding)
|
||||
.child(
|
||||
@@ -2092,9 +2090,8 @@ impl EditorElement {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(jump_data.clone(), |el, jump_data| {
|
||||
el.child(Icon::new(IconName::ArrowUpRight))
|
||||
.cursor_pointer()
|
||||
.when_some(jump_data.clone(), |this, jump_data| {
|
||||
this.cursor_pointer()
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Jump to File",
|
||||
@@ -2167,7 +2164,7 @@ impl EditorElement {
|
||||
}))
|
||||
} else {
|
||||
v_flex()
|
||||
.id(("excerpt header", EntityId::from(block_id)))
|
||||
.id(("excerpt header", block_id))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
@@ -2315,54 +2312,49 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
TransformBlock::ExcerptFooter { id, .. } => {
|
||||
let element = v_flex()
|
||||
.id(("excerpt footer", EntityId::from(block_id)))
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding + gutter_dimensions.margin))
|
||||
.h_full()
|
||||
.child(
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::ArrowDownFromLine.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group("")
|
||||
.hover(|style| {
|
||||
style.text_color(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_active_line_number,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
let id = *id;
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(
|
||||
id,
|
||||
multi_buffer::ExpandExcerptDirection::Down,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
cx,
|
||||
let element = v_flex().id(("excerpt footer", block_id)).size_full().child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding + gutter_dimensions.margin))
|
||||
.h_full()
|
||||
.child(
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::ArrowDownFromLine.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group("")
|
||||
.hover(|style| {
|
||||
style.text_color(
|
||||
cx.theme().colors().editor_active_line_number,
|
||||
)
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
let id = *id;
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(
|
||||
id,
|
||||
multi_buffer::ExpandExcerptDirection::Down,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
element.into_any()
|
||||
}
|
||||
};
|
||||
@@ -2378,8 +2370,8 @@ impl EditorElement {
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(block.height() as f32 * line_height),
|
||||
);
|
||||
let block_id = block.id();
|
||||
let (element, element_size) = render_block(block, available_space, block_id, row, cx);
|
||||
block_id += 1;
|
||||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||||
blocks.push(BlockLayout {
|
||||
row,
|
||||
@@ -2407,8 +2399,8 @@ impl EditorElement {
|
||||
AvailableSpace::Definite(width),
|
||||
AvailableSpace::Definite(block.height() as f32 * line_height),
|
||||
);
|
||||
let block_id = block.id();
|
||||
let (element, _) = render_block(block, available_space, block_id, row, cx);
|
||||
block_id += 1;
|
||||
blocks.push(BlockLayout {
|
||||
row,
|
||||
element,
|
||||
@@ -3923,7 +3915,7 @@ fn render_inline_blame_entry(
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> AnyElement {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
|
||||
|
||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||
let text = format!("{}, {}", author, relative_timestamp);
|
||||
@@ -3969,7 +3961,7 @@ fn render_blame_entry(
|
||||
};
|
||||
last_used_color.replace((sha_color, blame_entry.sha));
|
||||
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
|
||||
|
||||
let short_commit_id = blame_entry.sha.display_short();
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use file_icons::FileIcons;
|
||||
use futures::future::try_join_all;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
@@ -17,13 +16,10 @@ use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
};
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
use project::{
|
||||
project_settings::ProjectSettings, search::SearchQuery, FormatTrigger, Item as _, Project,
|
||||
ProjectPath,
|
||||
};
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
use workspace::item::{ItemSettings, TabContentParams};
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -38,9 +34,9 @@ use text::{BufferId, Selection};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
@@ -53,6 +49,7 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<Workspace>,
|
||||
remote_id: ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
@@ -66,6 +63,7 @@ impl FollowableItem for Editor {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let replica_id = project.read(cx).replica_id();
|
||||
let buffer_ids = state
|
||||
.excerpts
|
||||
@@ -79,55 +77,72 @@ impl FollowableItem for Editor {
|
||||
.collect::<Result<Vec<_>>>()
|
||||
});
|
||||
|
||||
let pane = pane.downgrade();
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let mut buffers = futures::future::try_join_all(buffers?)
|
||||
.await
|
||||
.debug_assert_ok("leaders don't share views for unshared buffers")?;
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
let multibuffer = cx.new_model(|cx| {
|
||||
let mut multibuffer;
|
||||
if state.singleton && buffers.len() == 1 {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer = MultiBuffer::new(replica_id, project.read(cx).capability());
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(title) = &state.title {
|
||||
multibuffer = multibuffer.with_title(title.clone())
|
||||
}
|
||||
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
let editor = pane.update(&mut cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
let singleton_buffer_matches = state.singleton
|
||||
&& buffers.first()
|
||||
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
ids_match || singleton_buffer_matches
|
||||
})
|
||||
})?;
|
||||
|
||||
let editor = if let Some(editor) = editor {
|
||||
editor
|
||||
} else {
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
let multibuffer = cx.new_model(|cx| {
|
||||
let mut multibuffer;
|
||||
if state.singleton && buffers.len() == 1 {
|
||||
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
|
||||
} else {
|
||||
multibuffer =
|
||||
MultiBuffer::new(replica_id, project.read(cx).capability());
|
||||
let mut excerpts = state.excerpts.into_iter().peekable();
|
||||
while let Some(excerpt) = excerpts.peek() {
|
||||
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_excerpts = iter::from_fn(|| {
|
||||
let excerpt = excerpts.peek()?;
|
||||
(excerpt.buffer_id == u64::from(buffer_id))
|
||||
.then(|| excerpts.next().unwrap())
|
||||
});
|
||||
let buffer =
|
||||
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
multibuffer.push_excerpts(
|
||||
buffer.clone(),
|
||||
buffer_excerpts.filter_map(deserialize_excerpt_range),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(title) = &state.title {
|
||||
multibuffer = multibuffer.with_title(title.clone())
|
||||
}
|
||||
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
})?
|
||||
};
|
||||
|
||||
update_editor_from_message(
|
||||
editor.downgrade(),
|
||||
project,
|
||||
@@ -312,16 +327,6 @@ impl FollowableItem for Editor {
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
|
||||
let self_singleton = self.buffer.read(cx).as_singleton()?;
|
||||
let other_singleton = existing.buffer.read(cx).as_singleton()?;
|
||||
if self_singleton == other_singleton {
|
||||
Some(Dedup::KeepExisting)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_editor_from_message(
|
||||
@@ -591,20 +596,6 @@ impl Item for Editor {
|
||||
Some(path.to_string_lossy().to_string().into())
|
||||
}
|
||||
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
|
||||
ItemSettings::get_global(cx)
|
||||
.file_icons
|
||||
.then(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||
.and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
|
||||
})
|
||||
.flatten()
|
||||
.map(|icon| Icon::from_path(icon))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||
self.buffer()
|
||||
@@ -855,8 +846,54 @@ impl Item for Editor {
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, _: &mut ViewContext<Self>) {
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||
let Some(workspace_id) = workspace.database_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let item_id = cx.view().item_id().as_u64() as ItemId;
|
||||
|
||||
fn serialize(
|
||||
buffer: Model<Buffer>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
let path = file.abs_path(cx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
DB.save_path(item_id, workspace_id, path.clone())
|
||||
.await
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
serialize(buffer.clone(), workspace_id, item_id, cx);
|
||||
|
||||
cx.subscribe(&buffer, |this, buffer, event, cx| {
|
||||
if let Some((_, Some(workspace_id))) = this.workspace.as_ref() {
|
||||
if let language::Event::FileHandleChanged = event {
|
||||
serialize(
|
||||
buffer,
|
||||
*workspace_id,
|
||||
cx.view().item_id().as_u64() as ItemId,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("Editor")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
||||
@@ -892,20 +929,6 @@ impl Item for Editor {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableItem for Editor {
|
||||
fn serialized_item_kind() -> &'static str {
|
||||
"Editor"
|
||||
}
|
||||
|
||||
fn cleanup(
|
||||
workspace_id: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|_| DB.delete_unloaded_items(workspace_id, alive_items))
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: Model<Project>,
|
||||
@@ -914,171 +937,41 @@ impl SerializableItem for Editor {
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let path_content_language = match DB
|
||||
.get_path_and_contents(item_id, workspace_id)
|
||||
.context("Failed to query editor state")
|
||||
{
|
||||
Ok(Some((path, content, language))) => {
|
||||
if ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers
|
||||
{
|
||||
(path, content, language)
|
||||
} else {
|
||||
(path, None, None)
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
return Task::ready(Err(anyhow!("No path or contents found for buffer")));
|
||||
}
|
||||
Err(error) => {
|
||||
return Task::ready(Err(error));
|
||||
}
|
||||
};
|
||||
let project_item: Result<_> = project.update(cx, |project, cx| {
|
||||
// Look up the path with this key associated, create a self with that path
|
||||
let path = DB
|
||||
.get_path(item_id, workspace_id)?
|
||||
.context("No path stored for this editor")?;
|
||||
|
||||
match path_content_language {
|
||||
(None, Some(content), language_name) => cx.spawn(|_, mut cx| async move {
|
||||
let language = if let Some(language_name) = language_name {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
let (worktree, path) = project
|
||||
.find_local_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: path.into(),
|
||||
};
|
||||
|
||||
Some(language_registry.language_for_name(&language_name).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(project.open_path(project_path, cx))
|
||||
});
|
||||
|
||||
// First create the empty buffer
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_local_buffer("", language, cx)
|
||||
})?;
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let (_, project_item) = project_item.await?;
|
||||
let buffer = project_item
|
||||
.downcast::<Buffer>()
|
||||
.map_err(|_| anyhow!("Project item at stored path was not a buffer"))?;
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
|
||||
// Then set the text so that the dirty bit is set correctly
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(content, cx);
|
||||
})?;
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
}),
|
||||
(Some(path), contents, _) => {
|
||||
let project_item = project.update(cx, |project, cx| {
|
||||
let (worktree, path) = project
|
||||
.find_worktree(&path, cx)
|
||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: path.into(),
|
||||
};
|
||||
|
||||
Ok(project.open_path(project_path, cx))
|
||||
});
|
||||
|
||||
project_item
|
||||
.map(|project_item| {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
let (_, project_item) = project_item.await?;
|
||||
let buffer = project_item.downcast::<Buffer>().map_err(|_| {
|
||||
anyhow!("Project item at stored path was not a buffer")
|
||||
})?;
|
||||
|
||||
// This is a bit wasteful: we're loading the whole buffer from
|
||||
// disk and then overwrite the content.
|
||||
// But for now, it keeps the implementation of the content serialization
|
||||
// simple, because we don't have to persist all of the metadata that we get
|
||||
// by loading the file (git diff base, mtime, ...).
|
||||
if let Some(buffer_text) = contents {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(buffer_text, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
})
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
_ => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
item_id: ItemId,
|
||||
closing: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
|
||||
|
||||
let project = self.project.clone()?;
|
||||
if project.read(cx).visible_worktrees(cx).next().is_none() {
|
||||
// If we don't have a worktree, we don't serialize, because
|
||||
// projects without worktrees aren't deserialized.
|
||||
serialize_dirty_buffers = false;
|
||||
}
|
||||
|
||||
if closing && !serialize_dirty_buffers {
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace_id = workspace.database_id()?;
|
||||
|
||||
let buffer = self.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let is_dirty = buffer.read(cx).is_dirty();
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| file.abs_path(cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
Some(cx.spawn(|_this, cx| async move {
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
if let Some(path) = path {
|
||||
DB.save_path(item_id, workspace_id, path.clone())
|
||||
.await
|
||||
.context("failed to save path of buffer")?
|
||||
}
|
||||
|
||||
if serialize_dirty_buffers {
|
||||
let (contents, language) = if is_dirty {
|
||||
let contents = snapshot.text();
|
||||
let language = snapshot.language().map(|lang| lang.name().to_string());
|
||||
(Some(contents), language)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
DB.save_contents(item_id, workspace_id, contents, language)
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.context("failed to save contents of buffer")?;
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,10 +74,10 @@ pub fn deploy_context_menu(
|
||||
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
||||
let menu = custom(editor, point, cx);
|
||||
editor.custom_context_menu = Some(custom);
|
||||
let Some(menu) = menu else {
|
||||
if menu.is_none() {
|
||||
return;
|
||||
};
|
||||
menu
|
||||
}
|
||||
menu.unwrap()
|
||||
} else {
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
@@ -85,13 +85,11 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
|
||||
let display_map = editor.selections.display_map(cx);
|
||||
let buffer = &editor.snapshot(cx).buffer_snapshot;
|
||||
let anchor = buffer.anchor_before(point.to_point(&display_map));
|
||||
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_anchor_range(anchor..anchor, SelectMode::Character);
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use anyhow::Result;
|
||||
use db::sqlez::statement::Statement;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
@@ -12,12 +10,10 @@ define_connection!(
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
// workspace_id: usize,
|
||||
// path: Option<PathBuf>,
|
||||
// path: PathBuf,
|
||||
// scroll_top_row: usize,
|
||||
// scroll_vertical_offset: f32,
|
||||
// scroll_horizontal_offset: f32,
|
||||
// content: Option<String>,
|
||||
// language: Option<String>,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||
&[sql! (
|
||||
@@ -35,39 +31,13 @@ define_connection!(
|
||||
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
|
||||
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
|
||||
),
|
||||
sql! (
|
||||
// Since sqlite3 doesn't support ALTER COLUMN, we create a new
|
||||
// table, move the data over, drop the old table, rename new table.
|
||||
CREATE TABLE new_editors_tmp (
|
||||
item_id INTEGER NOT NULL,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
path BLOB, // <-- No longer "NOT NULL"
|
||||
scroll_top_row INTEGER NOT NULL DEFAULT 0,
|
||||
scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
|
||||
scroll_vertical_offset REAL NOT NULL DEFAULT 0,
|
||||
contents TEXT, // New
|
||||
language TEXT, // New
|
||||
PRIMARY KEY(item_id, workspace_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
|
||||
SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
|
||||
FROM editors;
|
||||
|
||||
DROP TABLE editors;
|
||||
|
||||
ALTER TABLE new_editors_tmp RENAME TO editors;
|
||||
)];
|
||||
);
|
||||
|
||||
impl EditorDb {
|
||||
query! {
|
||||
pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(Option<PathBuf>, Option<String>, Option<String>)>> {
|
||||
SELECT path, contents, language FROM editors
|
||||
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
SELECT path FROM editors
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
@@ -85,20 +55,6 @@ impl EditorDb {
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option<String>, language: Option<String>) -> Result<()> {
|
||||
INSERT INTO editors
|
||||
(item_id, workspace_id, contents, language)
|
||||
VALUES
|
||||
(?1, ?2, ?3, ?4)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
item_id = ?1,
|
||||
workspace_id = ?2,
|
||||
contents = ?3,
|
||||
language = ?4
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the scroll top row, and offset
|
||||
query! {
|
||||
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
|
||||
@@ -124,75 +80,4 @@ impl EditorDb {
|
||||
WHERE item_id = ?1 AND workspace_id = ?2
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_saving_content() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||
|
||||
// Sanity check: make sure there is no row in the `editors` table
|
||||
assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None);
|
||||
|
||||
// Save content/language
|
||||
DB.save_contents(
|
||||
1234,
|
||||
workspace_id,
|
||||
Some("testing".into()),
|
||||
Some("Go".into()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that it can be read from DB
|
||||
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||
let (path, contents, language) = path_and_contents.unwrap();
|
||||
assert!(path.is_none());
|
||||
assert_eq!(contents, Some("testing".to_owned()));
|
||||
assert_eq!(language, Some("Go".to_owned()));
|
||||
|
||||
// Update it with NULL
|
||||
DB.save_contents(1234, workspace_id, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that it worked
|
||||
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||
let (path, contents, language) = path_and_contents.unwrap();
|
||||
assert!(path.is_none());
|
||||
assert!(contents.is_none());
|
||||
assert!(language.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,10 +476,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let Some(visible_line_count) = self.visible_line_count() else {
|
||||
return;
|
||||
};
|
||||
let new_pos = cur_position + point(0., amount.lines(visible_line_count));
|
||||
let new_pos = cur_position + point(0., amount.lines(self));
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::Editor;
|
||||
use serde::Deserialize;
|
||||
use ui::{px, Pixels};
|
||||
|
||||
@@ -10,16 +11,19 @@ pub enum ScrollAmount {
|
||||
}
|
||||
|
||||
impl ScrollAmount {
|
||||
pub fn lines(&self, mut visible_line_count: f32) -> f32 {
|
||||
pub fn lines(&self, editor: &mut Editor) -> f32 {
|
||||
match self {
|
||||
Self::Line(count) => *count,
|
||||
Self::Page(count) => {
|
||||
// for full pages subtract one to leave an anchor line
|
||||
if count.abs() == 1.0 {
|
||||
visible_line_count -= 1.0
|
||||
}
|
||||
(visible_line_count * count).trunc()
|
||||
}
|
||||
Self::Page(count) => editor
|
||||
.visible_line_count()
|
||||
.map(|mut l| {
|
||||
// for full pages subtract one to leave an anchor line
|
||||
if count.abs() == 1.0 {
|
||||
l -= 1.0
|
||||
}
|
||||
(l * count).trunc()
|
||||
})
|
||||
.unwrap_or(0.),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,18 +157,6 @@ impl SelectionsCollection {
|
||||
selections
|
||||
}
|
||||
|
||||
/// Returns the newest selection, adjusted to take into account the selection line_mode
|
||||
pub fn newest_adjusted(&self, cx: &mut AppContext) -> Selection<Point> {
|
||||
let mut selection = self.newest::<Point>(cx);
|
||||
if self.line_mode {
|
||||
let map = self.display_map(cx);
|
||||
let new_range = map.expand_to_line(selection.range());
|
||||
selection.start = new_range.start;
|
||||
selection.end = new_range.end;
|
||||
}
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn all_adjusted_display(
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
@@ -438,6 +426,46 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub(crate) fn set_pending_display_range(
|
||||
&mut self,
|
||||
range: Range<DisplayPoint>,
|
||||
mode: SelectMode,
|
||||
) {
|
||||
let (start, end, reversed) = {
|
||||
let display_map = self.display_map();
|
||||
let buffer = self.buffer();
|
||||
let mut start = range.start;
|
||||
let mut end = range.end;
|
||||
let reversed = if start > end {
|
||||
mem::swap(&mut start, &mut end);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let end_bias = if end > start { Bias::Left } else { Bias::Right };
|
||||
(
|
||||
buffer.anchor_before(start.to_point(&display_map)),
|
||||
buffer.anchor_at(end.to_point(&display_map), end_bias),
|
||||
reversed,
|
||||
)
|
||||
};
|
||||
|
||||
let new_pending = PendingSelection {
|
||||
selection: Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
start,
|
||||
end,
|
||||
reversed,
|
||||
goal: SelectionGoal::None,
|
||||
},
|
||||
mode,
|
||||
};
|
||||
|
||||
self.collection.pending = Some(new_pending);
|
||||
self.selections_changed = true;
|
||||
}
|
||||
|
||||
pub(crate) fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
|
||||
self.collection.pending = Some(PendingSelection { selection, mode });
|
||||
self.selections_changed = true;
|
||||
|
||||
@@ -79,7 +79,7 @@ impl EditorLspTestContext {
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.find_or_create_worktree("/root", true, cx)
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -456,9 +456,9 @@ impl ExtensionStore {
|
||||
let extension_ids = self
|
||||
.extension_index
|
||||
.extensions
|
||||
.iter()
|
||||
.filter(|(id, entry)| !entry.dev && extension_settings.should_auto_update(id))
|
||||
.map(|(id, _)| id.as_ref())
|
||||
.keys()
|
||||
.map(|id| id.as_ref())
|
||||
.filter(|id| extension_settings.should_auto_update(id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let task = self.fetch_extensions_from_api(
|
||||
|
||||
@@ -17,7 +17,6 @@ test-support = []
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
@@ -37,7 +36,6 @@ theme.workspace = true
|
||||
theme_selector.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vim.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod extension_card;
|
||||
mod feature_upsell;
|
||||
|
||||
pub use extension_card::*;
|
||||
pub use feature_upsell::*;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AnyElement, Div, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{prelude::*, ButtonLike};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct FeatureUpsell {
|
||||
base: Div,
|
||||
telemetry: Arc<Telemetry>,
|
||||
text: SharedString,
|
||||
docs_url: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl FeatureUpsell {
|
||||
pub fn new(telemetry: Arc<Telemetry>, text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: h_flex(),
|
||||
telemetry,
|
||||
text: text.into(),
|
||||
docs_url: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn docs_url(mut self, docs_url: impl Into<SharedString>) -> Self {
|
||||
self.docs_url = Some(docs_url.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for FeatureUpsell {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
// Style methods.
|
||||
impl FeatureUpsell {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
|
||||
gpui::border_style_methods!({
|
||||
visibility: pub
|
||||
});
|
||||
}
|
||||
|
||||
impl RenderOnce for FeatureUpsell {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
self.base
|
||||
.p_4()
|
||||
.justify_between()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(v_flex().overflow_hidden().child(Label::new(self.text)))
|
||||
.child(h_flex().gap_2().children(self.children).when_some(
|
||||
self.docs_url,
|
||||
|el, docs_url| {
|
||||
el.child(
|
||||
ButtonLike::new("open_docs")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("View docs"))
|
||||
.child(Icon::new(IconName::ArrowUpRight)),
|
||||
)
|
||||
.on_click({
|
||||
let telemetry = self.telemetry.clone();
|
||||
let docs_url = docs_url.clone();
|
||||
move |_event, cx| {
|
||||
telemetry.report_app_event(format!(
|
||||
"feature upsell: viewed docs ({docs_url})"
|
||||
));
|
||||
cx.open_url(&docs_url)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,35 @@ mod components;
|
||||
mod extension_suggest;
|
||||
mod extension_version_selector;
|
||||
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::components::ExtensionCard;
|
||||
use crate::extension_version_selector::{
|
||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||
};
|
||||
use client::telemetry::Telemetry;
|
||||
use client::ExtensionMetadata;
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, FontStyle,
|
||||
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use project::DirectoryLister;
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use std::ops::DerefMut;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||
use vim::VimModeSetting;
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::item::TabContentParams;
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
use crate::components::{ExtensionCard, FeatureUpsell};
|
||||
use crate::extension_version_selector::{
|
||||
ExtensionVersionSelector, ExtensionVersionSelectorDelegate,
|
||||
};
|
||||
|
||||
actions!(zed, [Extensions, InstallDevExtension]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@@ -54,35 +50,17 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
|
||||
}
|
||||
})
|
||||
.register_action(move |workspace, _: &InstallDevExtension, cx| {
|
||||
.register_action(move |_, _: &InstallDevExtension, cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let prompt = workspace.prompt_for_open_path(
|
||||
gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
},
|
||||
DirectoryLister::Local(workspace.app_state().fs.clone()),
|
||||
cx,
|
||||
);
|
||||
let prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
cx.deref_mut()
|
||||
.spawn(|mut cx| async move {
|
||||
let extension_path =
|
||||
match Flatten::flatten(prompt.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => paths.pop()?,
|
||||
Ok(None) => return None,
|
||||
Err(err) => {
|
||||
workspace_handle
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_portal_error(err.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let extension_path = prompt.await.log_err()??.pop()?;
|
||||
store
|
||||
.update(&mut cx, |store, cx| {
|
||||
store
|
||||
@@ -131,41 +109,6 @@ impl ExtensionFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum Feature {
|
||||
Git,
|
||||
Vim,
|
||||
LanguageBash,
|
||||
LanguageC,
|
||||
LanguageCpp,
|
||||
LanguageGo,
|
||||
LanguagePython,
|
||||
LanguageReact,
|
||||
LanguageRust,
|
||||
LanguageTypescript,
|
||||
}
|
||||
|
||||
fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
|
||||
static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<Feature, Vec<&'static str>>> = OnceLock::new();
|
||||
KEYWORDS_BY_FEATURE.get_or_init(|| {
|
||||
BTreeMap::from_iter([
|
||||
(Feature::Git, vec!["git"]),
|
||||
(Feature::Vim, vec!["vim"]),
|
||||
(Feature::LanguageBash, vec!["sh", "bash"]),
|
||||
(Feature::LanguageC, vec!["c", "clang"]),
|
||||
(Feature::LanguageCpp, vec!["c++", "cpp", "clang"]),
|
||||
(Feature::LanguageGo, vec!["go", "golang"]),
|
||||
(Feature::LanguagePython, vec!["python", "py"]),
|
||||
(Feature::LanguageReact, vec!["react"]),
|
||||
(Feature::LanguageRust, vec!["rust", "rs"]),
|
||||
(
|
||||
Feature::LanguageTypescript,
|
||||
vec!["type", "typescript", "ts"],
|
||||
),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ExtensionsPage {
|
||||
workspace: WeakView<Workspace>,
|
||||
list: UniformListScrollHandle,
|
||||
@@ -179,7 +122,6 @@ pub struct ExtensionsPage {
|
||||
query_contains_error: bool,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
extension_fetch_task: Option<Task<()>>,
|
||||
upsells: BTreeSet<Feature>,
|
||||
}
|
||||
|
||||
impl ExtensionsPage {
|
||||
@@ -218,7 +160,6 @@ impl ExtensionsPage {
|
||||
extension_fetch_task: None,
|
||||
_subscriptions: subscriptions,
|
||||
query_editor,
|
||||
upsells: BTreeSet::default(),
|
||||
};
|
||||
this.fetch_extensions(None, cx);
|
||||
this
|
||||
@@ -777,19 +718,25 @@ impl ExtensionsPage {
|
||||
cx.theme().colors().border
|
||||
};
|
||||
|
||||
h_flex().w_full().gap_2().key_context(key_context).child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.border_color(editor_border)
|
||||
.min_w(rems_from_px(384.))
|
||||
.rounded_lg()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(self.render_text_input(&self.query_editor, cx)),
|
||||
)
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.key_context(key_context)
|
||||
// .capture_action(cx.listener(Self::tab))
|
||||
// .on_action(cx.listener(Self::dismiss))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.border_color(editor_border)
|
||||
.min_w(rems_from_px(384.))
|
||||
.rounded_lg()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(self.render_text_input(&self.query_editor, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
@@ -832,7 +779,6 @@ impl ExtensionsPage {
|
||||
if let editor::EditorEvent::Edited { .. } = event {
|
||||
self.query_contains_error = false;
|
||||
self.fetch_extensions_debounced(cx);
|
||||
self.refresh_feature_upsells(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,120 +850,6 @@ impl ExtensionsPage {
|
||||
|
||||
Label::new(message)
|
||||
}
|
||||
|
||||
fn update_settings<T: Settings>(
|
||||
&mut self,
|
||||
selection: &Selection,
|
||||
cx: &mut ViewContext<Self>,
|
||||
callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
|
||||
) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let selection = *selection;
|
||||
settings::update_settings_file::<T>(fs, cx, move |settings| {
|
||||
let value = match selection {
|
||||
Selection::Unselected => false,
|
||||
Selection::Selected => true,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
callback(settings, value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_feature_upsells(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let Some(search) = self.search_query(cx) else {
|
||||
self.upsells.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
let search = search.to_lowercase();
|
||||
let search_terms = search
|
||||
.split_whitespace()
|
||||
.map(|term| term.trim())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (feature, keywords) in keywords_by_feature() {
|
||||
if keywords
|
||||
.iter()
|
||||
.any(|keyword| search_terms.contains(keyword))
|
||||
{
|
||||
self.upsells.insert(*feature);
|
||||
} else {
|
||||
self.upsells.remove(&feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_feature_upsells(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let upsells_count = self.upsells.len();
|
||||
|
||||
v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let upsell = match feature {
|
||||
Feature::Git => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed comes with basic Git support. More Git features are coming in the future.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git"),
|
||||
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/vim")
|
||||
.child(CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
Label::new("Enable vim mode"),
|
||||
if VimModeSetting::get_global(cx).0 {
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
cx.listener(move |this, selection, cx| {
|
||||
this.telemetry
|
||||
.report_app_event("feature upsell: toggle vim".to_string());
|
||||
this.update_settings::<VimModeSetting>(
|
||||
selection,
|
||||
cx,
|
||||
|setting, value| *setting = Some(value),
|
||||
);
|
||||
}),
|
||||
)),
|
||||
Feature::LanguageBash => {
|
||||
FeatureUpsell::new(telemetry, "Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash")
|
||||
}
|
||||
Feature::LanguageC => {
|
||||
FeatureUpsell::new(telemetry, "C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c")
|
||||
}
|
||||
Feature::LanguageCpp => {
|
||||
FeatureUpsell::new(telemetry, "C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp")
|
||||
}
|
||||
Feature::LanguageGo => {
|
||||
FeatureUpsell::new(telemetry, "Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go")
|
||||
}
|
||||
Feature::LanguagePython => {
|
||||
FeatureUpsell::new(telemetry, "Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python")
|
||||
}
|
||||
Feature::LanguageReact => {
|
||||
FeatureUpsell::new(telemetry, "React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
Feature::LanguageRust => {
|
||||
FeatureUpsell::new(telemetry, "Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust")
|
||||
}
|
||||
Feature::LanguageTypescript => {
|
||||
FeatureUpsell::new(telemetry, "Typescript support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
};
|
||||
|
||||
upsell.when(ix < upsells_count, |upsell| upsell.border_b_1())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExtensionsPage {
|
||||
@@ -1100,7 +932,6 @@ impl Render for ExtensionsPage {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(self.render_feature_upsells(cx))
|
||||
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
||||
let mut count = self.filtered_remote_extension_indices.len();
|
||||
if self.filter.include_dev_extensions() {
|
||||
@@ -1134,8 +965,14 @@ impl FocusableView for ExtensionsPage {
|
||||
impl Item for ExtensionsPage {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("Extensions".into())
|
||||
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
|
||||
Label::new("Extensions")
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
|
||||
@@ -34,11 +34,6 @@ impl FeatureFlag for TerminalInlineAssist {
|
||||
const NAME: &'static str = "terminal-inline-assist";
|
||||
}
|
||||
|
||||
pub struct GroupedDiagnostics {}
|
||||
impl FeatureFlag for GroupedDiagnostics {
|
||||
const NAME: &'static str = "grouped-diagnostics";
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
mod file_finder_tests;
|
||||
|
||||
mod new_path_prompt;
|
||||
mod open_path_prompt;
|
||||
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
@@ -14,7 +13,6 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use new_path_prompt::NewPathPrompt;
|
||||
use open_path_prompt::OpenPathPrompt;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use settings::Settings;
|
||||
@@ -43,7 +41,6 @@ pub struct FileFinder {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(FileFinder::register).detach();
|
||||
cx.observe_new_views(NewPathPrompt::register).detach();
|
||||
cx.observe_new_views(OpenPathPrompt::register).detach();
|
||||
}
|
||||
|
||||
impl FileFinder {
|
||||
@@ -679,7 +676,7 @@ impl FileFinderDelegate {
|
||||
let update_result = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
if let Some((worktree, relative_path)) =
|
||||
project.find_worktree(query_path, cx)
|
||||
project.find_local_worktree(query_path, cx)
|
||||
{
|
||||
path_matches.push(ProjectPanelOrdMatch(PathMatch {
|
||||
score: 1.0,
|
||||
|
||||
@@ -767,7 +767,7 @@ async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/external-src", false, cx)
|
||||
project.find_or_create_local_worktree("/external-src", false, cx)
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
@@ -1513,7 +1513,7 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.find_or_create_worktree("/test/project_2", true, cx)
|
||||
.find_or_create_local_worktree("/test/project_2", true, cx)
|
||||
.into_future()
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -197,12 +197,14 @@ pub struct NewPathDelegate {
|
||||
}
|
||||
|
||||
impl NewPathPrompt {
|
||||
pub(crate) fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
|
||||
workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Self::prompt_for_new_path(workspace, tx, cx);
|
||||
rx
|
||||
}));
|
||||
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
if workspace.project().read(cx).is_remote() {
|
||||
workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Self::prompt_for_new_path(workspace, tx, cx);
|
||||
rx
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{compare_paths, DirectoryLister};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::{prelude::*, LabelLike, ListItemSpacing};
|
||||
use ui::{ListItem, ViewContext};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OpenPathPrompt;
|
||||
|
||||
pub struct OpenPathDelegate {
|
||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||
lister: DirectoryLister,
|
||||
selected_index: usize,
|
||||
directory_state: Option<DirectoryState>,
|
||||
matches: Vec<usize>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
}
|
||||
|
||||
struct DirectoryState {
|
||||
path: String,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
error: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl OpenPathPrompt {
|
||||
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.set_prompt_for_open_path(Box::new(|workspace, lister, cx| {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Self::prompt_for_open_path(workspace, lister, tx, cx);
|
||||
rx
|
||||
}));
|
||||
}
|
||||
|
||||
fn prompt_for_open_path(
|
||||
workspace: &mut Workspace,
|
||||
lister: DirectoryLister,
|
||||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
let delegate = OpenPathDelegate {
|
||||
tx: Some(tx),
|
||||
lister: lister.clone(),
|
||||
selected_index: 0,
|
||||
directory_state: None,
|
||||
matches: Vec::new(),
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
};
|
||||
|
||||
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, cx);
|
||||
picker
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for OpenPathDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let lister = self.lister.clone();
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir == "" {
|
||||
dir = "/".to_string();
|
||||
}
|
||||
|
||||
let query = if self
|
||||
.directory_state
|
||||
.as_ref()
|
||||
.map_or(false, |s| s.path == dir)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(lister.list_directory(dir.clone(), cx))
|
||||
};
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(query) = query {
|
||||
let paths = query.await;
|
||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.delegate.directory_state = Some(match paths {
|
||||
Ok(mut paths) => {
|
||||
paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
|
||||
let match_candidates = paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, path)| {
|
||||
StringMatchCandidate::new(ix, path.to_string_lossy().into())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
DirectoryState {
|
||||
match_candidates,
|
||||
path: dir,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(err) => DirectoryState {
|
||||
match_candidates: vec![],
|
||||
path: dir,
|
||||
error: Some(err.to_string().into()),
|
||||
},
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let match_candidates = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let directory_state = this.delegate.directory_state.as_ref()?;
|
||||
if directory_state.error.is_some() {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(directory_state.match_candidates.clone())
|
||||
})
|
||||
.unwrap_or(None);
|
||||
|
||||
let Some(mut match_candidates) = match_candidates else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !suffix.starts_with('.') {
|
||||
match_candidates.retain(|m| !m.string.starts_with('.'));
|
||||
}
|
||||
|
||||
if suffix == "" {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(match_candidates.iter().map(|m| m.id));
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&match_candidates.as_slice(),
|
||||
&suffix,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
this.delegate.matches.sort_by_key(|m| {
|
||||
(
|
||||
this.delegate.directory_state.as_ref().and_then(|d| {
|
||||
d.match_candidates
|
||||
.get(*m)
|
||||
.map(|c| !c.string.starts_with(&suffix))
|
||||
}),
|
||||
*m,
|
||||
)
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm_completion(&self, query: String) -> Option<String> {
|
||||
Some(
|
||||
maybe!({
|
||||
let m = self.matches.get(self.selected_index)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
Some(format!("{}/{}", directory_state.path, candidate.string))
|
||||
})
|
||||
.unwrap_or(query),
|
||||
)
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(m) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
let Some(directory_state) = self.directory_state.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(candidate) = directory_state.match_candidates.get(*m) else {
|
||||
return;
|
||||
};
|
||||
let result = Path::new(&directory_state.path).join(&candidate.string);
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Some(vec![result])).ok();
|
||||
}
|
||||
cx.emit(gpui::DismissEvent);
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
self.should_dismiss
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
cx.emit(gpui::DismissEvent)
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let m = self.matches.get(ix)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.child(LabelLike::new().child(candidate.string.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
|
||||
error
|
||||
} else {
|
||||
"No such file or directory".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
Arc::from("[directory/]filename.ext")
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@ futures.workspace = true
|
||||
http.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -97,6 +97,7 @@ pub struct GenerateContentRequest {
|
||||
pub contents: Vec<Content>,
|
||||
pub generation_config: Option<GenerationConfig>,
|
||||
pub safety_settings: Option<Vec<SafetySetting>>,
|
||||
pub cached_content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -267,3 +268,115 @@ pub struct CountTokensRequest {
|
||||
pub struct CountTokensResponse {
|
||||
pub total_tokens: usize,
|
||||
}
|
||||
|
||||
pub async fn create_cached_content<T: HttpClient>(
|
||||
client: &T,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CreateCachedContentRequest,
|
||||
) -> Result<CreateCachedContentResponse> {
|
||||
let uri = format!("{}/v1beta/cachedContents?key={}", api_url, api_key);
|
||||
let request = serde_json::to_string(&request)?;
|
||||
let mut response = client.post_json(&uri, request.into()).await?;
|
||||
let mut text = String::new();
|
||||
response.body_mut().read_to_string(&mut text).await?;
|
||||
if response.status().is_success() {
|
||||
Ok(serde_json::from_str::<CreateCachedContentResponse>(&text)?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"error during createCachedContent, status code: {:?}, body: {}",
|
||||
response.status(),
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCachedContentRequest {
|
||||
pub contents: Vec<Content>,
|
||||
pub tools: Option<Vec<Tool>>,
|
||||
pub ttl: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub model: String,
|
||||
pub system_instruction: Option<Content>,
|
||||
pub tool_config: Option<ToolConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCachedContentResponse {
|
||||
pub name: String,
|
||||
pub create_time: chrono::DateTime<chrono::Utc>,
|
||||
pub update_time: chrono::DateTime<chrono::Utc>,
|
||||
pub expire_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub usage_metadata: UsageMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UsageMetadata {
|
||||
pub total_token_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tool {
|
||||
pub function_declarations: Vec<FunctionDeclaration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FunctionDeclaration {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: Schema,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Schema {
|
||||
#[serde(rename = "type")]
|
||||
pub schema_type: SchemaType,
|
||||
pub format: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub nullable: Option<bool>,
|
||||
pub enum_values: Option<Vec<String>>,
|
||||
pub properties: Option<std::collections::HashMap<String, Box<Schema>>>,
|
||||
pub required: Option<Vec<String>>,
|
||||
pub items: Option<Box<Schema>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum SchemaType {
|
||||
TypeUnspecified,
|
||||
String,
|
||||
Number,
|
||||
Integer,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolConfig {
|
||||
pub function_calling_config: Option<FunctionCallingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FunctionCallingConfig {
|
||||
pub mode: Option<FunctionCallingMode>,
|
||||
pub allowed_function_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum FunctionCallingMode {
|
||||
ModeUnspecified,
|
||||
Auto,
|
||||
Any,
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
|
||||
] }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
|
||||
oo7 = "0.3.0"
|
||||
open = "5.2.0"
|
||||
filedescriptor = "0.8.2"
|
||||
x11rb = { version = "0.13.0", features = [
|
||||
"allow-unsafe-code",
|
||||
@@ -135,7 +134,7 @@ x11rb = { version = "0.13.0", features = [
|
||||
"resource_manager",
|
||||
"sync",
|
||||
] }
|
||||
xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "fcbb4612185cc129ceeff51d22f7fb51810a03b2", features = [
|
||||
xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "2d4c4439160c7846ede0f0ece93bf73b1e613339", features = [
|
||||
"wayland",
|
||||
"x11",
|
||||
] }
|
||||
|
||||
@@ -22,7 +22,6 @@ actions!(
|
||||
struct TextInput {
|
||||
focus_handle: FocusHandle,
|
||||
content: SharedString,
|
||||
placeholder: SharedString,
|
||||
selected_range: Range<usize>,
|
||||
selection_reversed: bool,
|
||||
marked_range: Option<Range<usize>>,
|
||||
@@ -85,12 +84,7 @@ impl TextInput {
|
||||
|
||||
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
||||
self.is_selecting = true;
|
||||
|
||||
if event.modifiers.shift {
|
||||
self.select_to(self.index_for_mouse_position(event.position), cx);
|
||||
} else {
|
||||
self.move_to(self.index_for_mouse_position(event.position), cx)
|
||||
}
|
||||
self.move_to(self.index_for_mouse_position(event.position), cx)
|
||||
}
|
||||
|
||||
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext<Self>) {
|
||||
@@ -121,10 +115,6 @@ impl TextInput {
|
||||
}
|
||||
|
||||
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||
if self.content.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
|
||||
else {
|
||||
return 0;
|
||||
@@ -358,17 +348,10 @@ impl Element for TextElement {
|
||||
let selected_range = input.selected_range.clone();
|
||||
let cursor = input.cursor_offset();
|
||||
let style = cx.text_style();
|
||||
|
||||
let (display_text, text_color) = if content.is_empty() {
|
||||
(input.placeholder.clone(), hsla(0., 0., 0., 0.2))
|
||||
} else {
|
||||
(content.clone(), style.color)
|
||||
};
|
||||
|
||||
let run = TextRun {
|
||||
len: display_text.len(),
|
||||
len: input.content.len(),
|
||||
font: style.font(),
|
||||
color: text_color,
|
||||
color: style.color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
@@ -389,7 +372,7 @@ impl Element for TextElement {
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: display_text.len() - marked_range.end,
|
||||
len: input.content.len() - marked_range.end,
|
||||
..run.clone()
|
||||
},
|
||||
]
|
||||
@@ -403,7 +386,7 @@ impl Element for TextElement {
|
||||
let font_size = style.font_size.to_pixels(cx.rem_size());
|
||||
let line = cx
|
||||
.text_system()
|
||||
.shape_line(display_text, font_size, &runs)
|
||||
.shape_line(content, font_size, &runs)
|
||||
.unwrap();
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
@@ -602,7 +585,6 @@ fn main() {
|
||||
let text_input = cx.new_view(|cx| TextInput {
|
||||
focus_handle: cx.focus_handle(),
|
||||
content: "".into(),
|
||||
placeholder: "Type here...".into(),
|
||||
selected_range: 0..0,
|
||||
selection_reversed: false,
|
||||
marked_range: None,
|
||||
|
||||
@@ -58,7 +58,6 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
gpu: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use derive_more::{Deref, DerefMut};
|
||||
use futures::{channel::oneshot, future::LocalBoxFuture, Future};
|
||||
use slotmap::SlotMap;
|
||||
use smol::future::FutureExt;
|
||||
use time::UtcOffset;
|
||||
|
||||
pub use async_context::*;
|
||||
use collections::{FxHashMap, FxHashSet, VecDeque};
|
||||
@@ -611,11 +612,10 @@ impl AppContext {
|
||||
/// Displays a platform modal for selecting paths.
|
||||
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
/// May return an error on Linux if the file picker couldn't be opened.
|
||||
pub fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
self.platform.prompt_for_paths(options)
|
||||
}
|
||||
|
||||
@@ -623,11 +623,7 @@ impl AppContext {
|
||||
/// The provided directory will be used to set the initial location.
|
||||
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
/// May return an error on Linux if the file picker couldn't be opened.
|
||||
pub fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &Path,
|
||||
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
self.platform.prompt_for_new_path(directory)
|
||||
}
|
||||
|
||||
@@ -646,6 +642,11 @@ impl AppContext {
|
||||
self.platform.restart(binary_path)
|
||||
}
|
||||
|
||||
/// Returns the local timezone at the platform level.
|
||||
pub fn local_timezone(&self) -> UtcOffset {
|
||||
self.platform.local_timezone()
|
||||
}
|
||||
|
||||
/// Updates the http client assigned to GPUI
|
||||
pub fn update_http_client(&mut self, new_client: Arc<dyn HttpClient>) {
|
||||
self.http_client = new_client;
|
||||
|
||||
@@ -291,10 +291,6 @@ pub trait BorrowAppContext {
|
||||
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||
where
|
||||
G: Global;
|
||||
/// Updates the global state of the given type, creating a default if it didn't exist before.
|
||||
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||
where
|
||||
G: Global + Default;
|
||||
}
|
||||
|
||||
impl<C> BorrowAppContext for C
|
||||
@@ -314,14 +310,6 @@ where
|
||||
self.borrow_mut().end_global_lease(global);
|
||||
result
|
||||
}
|
||||
|
||||
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||
where
|
||||
G: Global + Default,
|
||||
{
|
||||
self.borrow_mut().default_global::<G>();
|
||||
self.update_global(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A flatten equivalent for anyhow `Result`s.
|
||||
|
||||
@@ -60,6 +60,7 @@ pub(crate) use mac::*;
|
||||
pub use semantic_version::SemanticVersion;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) use test::*;
|
||||
use time::UtcOffset;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use windows::*;
|
||||
|
||||
@@ -136,8 +137,8 @@ pub(crate) trait Platform: 'static {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||
fn reveal_path(&self, path: &Path);
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
@@ -158,6 +159,7 @@ pub(crate) trait Platform: 'static {
|
||||
""
|
||||
}
|
||||
fn app_path(&self) -> Result<PathBuf>;
|
||||
fn local_timezone(&self) -> UtcOffset;
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
@@ -252,16 +254,6 @@ pub enum Decorations {
|
||||
},
|
||||
}
|
||||
|
||||
/// A type to describe which GPU to prefer.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum Gpu {
|
||||
/// The window is rendered using the integrated (lower-power) GPU
|
||||
#[default]
|
||||
Integrated,
|
||||
/// The window is rendered using the discrete GPU
|
||||
Discrete,
|
||||
}
|
||||
|
||||
/// What window controls this platform supports
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct WindowControls {
|
||||
@@ -689,10 +681,6 @@ pub struct WindowOptions {
|
||||
/// Whether to use client or server side decorations. Wayland only
|
||||
/// Note that this may be ignored.
|
||||
pub window_decorations: Option<WindowDecorations>,
|
||||
|
||||
/// Whether to prefer one GPU over another. macOS only
|
||||
/// Note that this may be ignored.
|
||||
pub gpu: Option<Gpu>,
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -721,11 +709,6 @@ pub(crate) struct WindowParams {
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
|
||||
/// Whether to prefer one GPU over another. macOS only
|
||||
/// Note that this may be ignored.
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub gpu: Option<Gpu>,
|
||||
}
|
||||
|
||||
/// Represents the status of how a window should be opened.
|
||||
@@ -776,7 +759,6 @@ impl Default for WindowOptions {
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
gpu: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
|
||||
use crate::{
|
||||
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, Gpu, Hsla, MonochromeSprite,
|
||||
Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow,
|
||||
Size, Underline,
|
||||
AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, Hsla, MonochromeSprite, Path,
|
||||
PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
|
||||
Underline,
|
||||
};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use collections::HashMap;
|
||||
@@ -32,7 +32,6 @@ pub unsafe fn new_renderer(
|
||||
native_view: *mut c_void,
|
||||
bounds: crate::Size<f32>,
|
||||
transparent: bool,
|
||||
_gpu: Option<Gpu>,
|
||||
) -> Renderer {
|
||||
use raw_window_handle as rwh;
|
||||
struct RawWindow {
|
||||
|
||||
@@ -21,7 +21,6 @@ use std::{
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
||||
use ashpd::desktop::ResponseError;
|
||||
use ashpd::{url, ActivationToken};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
@@ -30,6 +29,7 @@ use filedescriptor::FileDescriptor;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use time::UtcOffset;
|
||||
use util::ResultExt;
|
||||
use wayland_client::Connection;
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
@@ -54,9 +54,6 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
||||
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||
|
||||
const FILE_PICKER_PORTAL_MISSING: &str =
|
||||
"Couldn't open file picker due to missing xdg-desktop-portal implementation.";
|
||||
|
||||
pub trait LinuxClient {
|
||||
fn compositor_name(&self) -> &'static str;
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||
@@ -259,84 +256,75 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let title = if options.directories {
|
||||
"Open Folder"
|
||||
let title = if options.multiple {
|
||||
if !options.files {
|
||||
"Open folders"
|
||||
} else {
|
||||
"Open files"
|
||||
}
|
||||
} else {
|
||||
"Open File"
|
||||
if !options.files {
|
||||
"Open folder"
|
||||
} else {
|
||||
"Open file"
|
||||
}
|
||||
};
|
||||
|
||||
let request = match OpenFileRequest::default()
|
||||
let result = OpenFileRequest::default()
|
||||
.modal(true)
|
||||
.title(title)
|
||||
.accept_label("Select")
|
||||
.multiple(options.multiple)
|
||||
.directory(options.directories)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
let result = match err {
|
||||
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let result = match request.response() {
|
||||
Ok(response) => Ok(Some(
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.iter()
|
||||
.filter_map(|uri| uri.to_file_path().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
.map(|uri| uri.to_file_path().ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let directory = directory.to_owned();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let request = match SaveFileRequest::default()
|
||||
let request = SaveFileRequest::default()
|
||||
.modal(true)
|
||||
.title("Save File")
|
||||
.current_folder(directory)
|
||||
.expect("pathbuf should not be nul terminated")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
let result = match err {
|
||||
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
.title("Select new path")
|
||||
.accept_label("Accept")
|
||||
.current_folder(directory);
|
||||
|
||||
let result = if let Ok(request) = request {
|
||||
request
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let result = match request.response() {
|
||||
Ok(response) => Ok(response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
@@ -396,6 +384,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
|
||||
@@ -510,26 +502,11 @@ pub(super) fn open_uri_internal(
|
||||
if let Some(uri) = url::Url::parse(uri).log_err() {
|
||||
executor
|
||||
.spawn(async move {
|
||||
match OpenUriRequest::default()
|
||||
.activation_token(activation_token.clone().map(ActivationToken::from))
|
||||
OpenUriRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send_uri(&uri)
|
||||
.await
|
||||
{
|
||||
Ok(_) => return,
|
||||
Err(e) => log::error!("Failed to open with dbus: {}", e),
|
||||
}
|
||||
|
||||
for mut command in open::commands(uri.to_string()) {
|
||||
if let Some(token) = activation_token.as_ref() {
|
||||
command.env("XDG_ACTIVATION_TOKEN", token);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => return,
|
||||
Err(e) => {
|
||||
log::error!("Failed to open with {:?}: {}", command.get_program(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -542,20 +519,12 @@ pub(super) fn reveal_path_internal(
|
||||
) {
|
||||
executor
|
||||
.spawn(async move {
|
||||
if let Some(dir) = File::open(path.clone()).log_err() {
|
||||
match OpenDirectoryRequest::default()
|
||||
if let Some(dir) = File::open(path).log_err() {
|
||||
OpenDirectoryRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send(&dir.as_fd())
|
||||
.await
|
||||
{
|
||||
Ok(_) => return,
|
||||
Err(e) => log::error!("Failed to open with dbus: {}", e),
|
||||
}
|
||||
if path.is_dir() {
|
||||
open::that_detached(path).log_err();
|
||||
} else {
|
||||
open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
|
||||
}
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -11,7 +11,6 @@ use calloop_wayland_source::WaylandSource;
|
||||
use collections::HashMap;
|
||||
use filedescriptor::Pipe;
|
||||
|
||||
use http::Url;
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
use wayland_backend::client::ObjectId;
|
||||
@@ -1120,10 +1119,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
let keyboard_focused_window = get_window(&mut state, &surface.id());
|
||||
state.keyboard_focused_window = None;
|
||||
state.enter_token.take();
|
||||
// Prevent keyboard events from repeating after opening e.g. a file chooser and closing it quickly
|
||||
state.repeat.current_id += 1;
|
||||
state.clipboard.set_offer(None);
|
||||
state.clipboard.set_primary_offer(None);
|
||||
|
||||
if let Some(window) = keyboard_focused_window {
|
||||
if let Some(ref mut compose) = state.compose_state {
|
||||
@@ -1796,8 +1792,7 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
||||
|
||||
let paths: SmallVec<[_; 2]> = file_list
|
||||
.lines()
|
||||
.filter_map(|path| Url::parse(path).log_err())
|
||||
.filter_map(|url| url.to_file_path().log_err())
|
||||
.map(|path| PathBuf::from(path.replace("file://", "")))
|
||||
.collect();
|
||||
let position = Point::new(x.into(), y.into());
|
||||
|
||||
|
||||
@@ -990,17 +990,20 @@ impl X11Client {
|
||||
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let keystroke = state.pre_ime_key_down.take();
|
||||
if !state.composing {
|
||||
if let Some(keystroke) = state.pre_ime_key_down.take() {
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
state.composing = false;
|
||||
drop(state);
|
||||
if let Some(mut keystroke) = keystroke {
|
||||
keystroke.ime_key = Some(text.clone());
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
}
|
||||
|
||||
window.handle_ime_commit(text);
|
||||
Some(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::metal_atlas::MetalAtlas;
|
||||
use crate::{
|
||||
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
|
||||
Gpu, Hsla, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
Hsla, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -38,9 +38,8 @@ pub unsafe fn new_renderer(
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
gpu: Option<Gpu>,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context, gpu)
|
||||
MetalRenderer::new(context)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -109,19 +108,13 @@ pub(crate) struct MetalRenderer {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, gpu: Option<Gpu>) -> Self {
|
||||
let device = match gpu {
|
||||
Some(Gpu::Discrete) => metal::Device::system_default(),
|
||||
_ => {
|
||||
// By default, prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
let mut devices = metal::Device::all();
|
||||
devices.sort_by_key(|device| (!device.is_removable(), device.is_low_power()));
|
||||
devices.pop()
|
||||
}
|
||||
};
|
||||
let Some(device) = device else {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
let mut devices = metal::Device::all();
|
||||
devices.sort_by_key(|device| (!device.is_removable(), device.is_low_power()));
|
||||
let Some(device) = devices.pop() else {
|
||||
log::error!("unable to access a compatible graphics device");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ use std::{
|
||||
slice, str,
|
||||
sync::Arc,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
use super::renderer;
|
||||
|
||||
@@ -601,7 +602,7 @@ impl Platform for MacPlatform {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
@@ -631,7 +632,7 @@ impl Platform for MacPlatform {
|
||||
};
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(Ok(result));
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
@@ -642,7 +643,7 @@ impl Platform for MacPlatform {
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
let directory = directory.to_owned();
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
@@ -664,7 +665,7 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(Ok(result));
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
@@ -759,6 +760,14 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
unsafe {
|
||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||
let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
|
||||
UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
|
||||
@@ -505,7 +505,6 @@ impl MacWindow {
|
||||
show,
|
||||
display_id,
|
||||
window_min_size,
|
||||
gpu,
|
||||
}: WindowParams,
|
||||
executor: ForegroundExecutor,
|
||||
renderer_context: renderer::Context,
|
||||
@@ -604,7 +603,6 @@ impl MacWindow {
|
||||
native_view as *mut _,
|
||||
bounds.size.map(|pixels| pixels.0),
|
||||
false,
|
||||
gpu,
|
||||
),
|
||||
request_frame_callback: None,
|
||||
event_callback: None,
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(crate) struct TestPlatform {
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TestPrompts {
|
||||
multiple_choice: VecDeque<oneshot::Sender<usize>>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
|
||||
}
|
||||
|
||||
impl TestPlatform {
|
||||
@@ -80,7 +80,7 @@ impl TestPlatform {
|
||||
.new_path
|
||||
.pop_front()
|
||||
.expect("no pending new path prompt");
|
||||
tx.send(Ok(select_path(&path))).ok();
|
||||
tx.send(select_path(&path)).ok();
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
|
||||
@@ -216,14 +216,14 @@ impl Platform for TestPlatform {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &std::path::Path,
|
||||
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.prompts
|
||||
.borrow_mut()
|
||||
@@ -257,6 +257,10 @@ impl Platform for TestPlatform {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn local_timezone(&self) -> time::UtcOffset {
|
||||
time::UtcOffset::UTC
|
||||
}
|
||||
|
||||
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<std::path::PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user