Merge branch 'main' into debugger
This commit is contained in:
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -43,6 +43,8 @@ jobs:
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
||||
export GIT_COMMITTER_NAME="Zed Bot"
|
||||
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
||||
git tag v${output}${tag_suffix}
|
||||
git push origin HEAD v${output}${tag_suffix}
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -232,7 +232,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
8
.github/workflows/deploy_cloudflare.yml
vendored
8
.github/workflows/deploy_cloudflare.yml
vendored
@@ -37,28 +37,28 @@ jobs:
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
||||
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
||||
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
||||
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
||||
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
136
Cargo.lock
generated
136
Cargo.lock
generated
@@ -9,7 +9,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -544,9 +544,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.13"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429"
|
||||
checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
@@ -4182,6 +4182,51 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-tar",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.19",
|
||||
"wasm-encoder 0.215.0",
|
||||
"wasmparser 0.215.0",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.11.5",
|
||||
"extension",
|
||||
"fs",
|
||||
"language",
|
||||
"log",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"theme",
|
||||
"tokio",
|
||||
"toml 0.8.19",
|
||||
"tree-sitter",
|
||||
"wasmtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension_host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
@@ -4192,6 +4237,7 @@ dependencies = [
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger 0.11.5",
|
||||
"extension",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
@@ -4219,36 +4265,11 @@ dependencies = [
|
||||
"ui",
|
||||
"url",
|
||||
"util",
|
||||
"wasm-encoder 0.215.0",
|
||||
"wasmparser 0.215.0",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
"wit-component",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"env_logger 0.11.5",
|
||||
"extension",
|
||||
"fs",
|
||||
"language",
|
||||
"log",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"theme",
|
||||
"tokio",
|
||||
"toml 0.8.19",
|
||||
"tree-sitter",
|
||||
"wasmtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extensions_ui"
|
||||
version = "0.1.0"
|
||||
@@ -4258,7 +4279,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -4478,9 +4499,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -5675,7 +5696,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.4.10",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -6247,9 +6268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-serde"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a444fb3f87ee6885eb316028cc998c7d84811663ef95d78c419419423d5a054"
|
||||
checksum = "77b96de099fc23d5c21e05de32cc087c8326983895b7f6c242562af01f7d4c81"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -6399,6 +6420,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"strum 0.25.0",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
"thiserror",
|
||||
@@ -6460,7 +6482,6 @@ dependencies = [
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -6537,9 +6558,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
version = "0.2.161"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -6581,7 +6602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6645,18 +6666,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a"
|
||||
checksum = "70fe496a7af8c406f877635cbf3cd6a9fac9d6f443f58691cd8afe6ce0971af4"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5"
|
||||
checksum = "b01f197a15988fb5b2ec0a5a9800c97e70771499c456ad757d63b3c5e9b96e75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7246,9 +7267,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146074ad45cab20f5d98ccded164826158471f21d04f96e40b9872529e10979d"
|
||||
checksum = "84f8a9ab08b34237c2c1d0504b794c2ff01c08dfc46a060d160f004a7f479c31"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -8021,9 +8042,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_geometry"
|
||||
@@ -8898,27 +8919,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
|
||||
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
@@ -9628,9 +9649,11 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"prost",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -10060,9 +10083,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "runtimelib"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "263588fe9593333c4bfde258c9021fc64e766ea434e070c6b67c7100536d6499"
|
||||
checksum = "bc7fe3c17675445fe89de68d130be00b7115104924fbcf53a9b0a84b0283fc81"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
@@ -12187,6 +12210,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"strum 0.25.0",
|
||||
"util",
|
||||
"uuid",
|
||||
]
|
||||
@@ -12217,7 +12241,6 @@ name = "theme_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -14837,7 +14860,6 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -15159,7 +15181,7 @@ dependencies = [
|
||||
"diagnostics",
|
||||
"editor",
|
||||
"env_logger 0.11.5",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
|
||||
@@ -33,6 +33,7 @@ members = [
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
"crates/extension_host",
|
||||
"crates/extensions_ui",
|
||||
"crates/feature_flags",
|
||||
"crates/feedback",
|
||||
@@ -210,6 +211,7 @@ debugger_tools = { path = "crates/debugger_tools" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
extension_host = { path = "crates/extension_host" }
|
||||
extensions_ui = { path = "crates/extensions_ui" }
|
||||
feature_flags = { path = "crates/feature_flags" }
|
||||
feedback = { path = "crates/feedback" }
|
||||
@@ -378,7 +380,7 @@ linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.3.1"
|
||||
nbformat = "0.3.2"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
@@ -411,7 +413,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.16.0", default-features = false, features = [
|
||||
runtimelib = { version = "0.16.1", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
|
||||
1
assets/icons/wand.svg
Normal file
1
assets/icons/wand.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -157,51 +157,6 @@
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w w": "workspace::ActivateNextPane",
|
||||
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
|
||||
"ctrl-w p": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w v": "pane::SplitVertical",
|
||||
"ctrl-w ctrl-v": "pane::SplitVertical",
|
||||
"ctrl-w s": "pane::SplitHorizontal",
|
||||
"ctrl-w shift-s": "pane::SplitHorizontal",
|
||||
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
||||
"ctrl-w c": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||
"ctrl-w q": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w d": "editor::GoToDefinitionSplit",
|
||||
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
||||
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
@@ -339,6 +294,10 @@
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-r": ["vim::PushOperator", "Register"],
|
||||
"insert": "vim::ToggleReplace"
|
||||
}
|
||||
@@ -357,6 +316,10 @@
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
@@ -371,7 +334,9 @@
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators",
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }]
|
||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||
"ctrl-q": ["vim::PushOperator", { "Literal": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -485,6 +450,49 @@
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == literal",
|
||||
"bindings": {
|
||||
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
|
||||
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
|
||||
"ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]],
|
||||
"ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]],
|
||||
"ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]],
|
||||
"ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]],
|
||||
"ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]],
|
||||
"ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]],
|
||||
"ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]],
|
||||
"ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]],
|
||||
"ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]],
|
||||
"ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]],
|
||||
"ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]],
|
||||
"ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]],
|
||||
"ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]],
|
||||
"ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]],
|
||||
"ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]],
|
||||
"ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]],
|
||||
"ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]],
|
||||
"ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]],
|
||||
"ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]],
|
||||
"ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]],
|
||||
"ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]],
|
||||
"ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]],
|
||||
"ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]],
|
||||
"ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]],
|
||||
"ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]],
|
||||
"ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]],
|
||||
"ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]],
|
||||
"ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]],
|
||||
"ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]],
|
||||
"ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]],
|
||||
"escape": ["vim::Literal", ["escape", "\u001B"]],
|
||||
"enter": ["vim::Literal", ["enter", "\u000D"]],
|
||||
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
||||
// zed extensions:
|
||||
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
||||
"delete": ["vim::Literal", ["delete", "\u007F"]]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
@@ -493,7 +501,57 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen",
|
||||
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
|
||||
"ctrl-w w": "workspace::ActivateNextPane",
|
||||
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
|
||||
"ctrl-w p": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
|
||||
"ctrl-w v": "pane::SplitVertical",
|
||||
"ctrl-w ctrl-v": "pane::SplitVertical",
|
||||
"ctrl-w s": "pane::SplitHorizontal",
|
||||
"ctrl-w shift-s": "pane::SplitHorizontal",
|
||||
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
||||
"ctrl-w c": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||
"ctrl-w q": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"g /": "pane::DeploySearch"
|
||||
|
||||
@@ -652,6 +652,12 @@
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
//
|
||||
// Whether or not do display the git commit summary on the same line.
|
||||
// "show_commit_summary": false
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
|
||||
@@ -16,7 +16,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||
@@ -369,7 +369,10 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
on_click: Some(Arc::new(|indicator, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ use std::sync::Arc;
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::slash_command::streaming_example_command;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
|
||||
actions!(
|
||||
@@ -468,6 +469,19 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_flag::<streaming_example_command::StreamingExampleSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry.register_command(
|
||||
streaming_example_command::StreamingExampleSlashCommand,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
update_slash_commands_from_settings(cx);
|
||||
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
|
||||
.detach();
|
||||
|
||||
@@ -73,12 +73,11 @@ use std::{
|
||||
};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use text::SelectionGoal;
|
||||
use ui::TintColor;
|
||||
use ui::{
|
||||
prelude::*,
|
||||
utils::{format_distance_from_now, DateTimeType},
|
||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::{
|
||||
@@ -4006,13 +4005,7 @@ impl Render for ContextEditor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let focus_handle = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
.capture_action(cx.listener(ContextEditor::cancel))
|
||||
@@ -4060,28 +4053,7 @@ impl Render for ContextEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(render_inject_context_menu(cx.view().downgrade(), cx))
|
||||
.child(
|
||||
IconButton::new("quote-button", IconName::Quote)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(QuoteSelection.boxed_clone());
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
cx.new_view(|cx| {
|
||||
Tooltip::new("Insert Selection").key_binding(
|
||||
focus_handle.as_ref().and_then(|handle| {
|
||||
KeyBinding::for_action_in(
|
||||
&QuoteSelection,
|
||||
&handle,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -4376,6 +4348,7 @@ fn render_inject_context_menu(
|
||||
Button::new("trigger", "Add Context")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
|
||||
)
|
||||
@@ -4550,7 +4523,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(Label::new("Insert Context"))
|
||||
.child(Label::new("Add Context"))
|
||||
.child(Label::new("/ command").color(Color::Muted))
|
||||
.into_any()
|
||||
},
|
||||
@@ -4574,7 +4547,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
}
|
||||
},
|
||||
)
|
||||
.action("Insert Selection", QuoteSelection.boxed_clone())
|
||||
.action("Add Selection", QuoteSelection.boxed_clone())
|
||||
}))
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -24,6 +24,7 @@ use gpui::{
|
||||
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
logging::report_assistant_event,
|
||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
@@ -1955,6 +1956,7 @@ impl Context {
|
||||
});
|
||||
|
||||
match event {
|
||||
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
@@ -2060,23 +2062,28 @@ impl Context {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(telemetry) = this.telemetry.as_ref() {
|
||||
let language_name = this
|
||||
.buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.map(|language| language.name());
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
let language_name = this
|
||||
.buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.map(|language| language.name());
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: Some(this.id.0.clone()),
|
||||
kind: AssistantKind::Panel,
|
||||
phase: AssistantPhase::Response,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
});
|
||||
}
|
||||
},
|
||||
this.telemetry.clone(),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
);
|
||||
|
||||
if let Ok(stop_reason) = result {
|
||||
match stop_reason {
|
||||
@@ -2543,7 +2550,7 @@ impl Context {
|
||||
let mut messages = stream.await?;
|
||||
|
||||
let mut replaced = !replace_old;
|
||||
while let Some(message) = messages.next().await {
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
let text = message?;
|
||||
let mut lines = text.lines();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
|
||||
@@ -21,9 +21,7 @@ use fs::Fs;
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
join,
|
||||
stream::{self, BoxStream},
|
||||
SinkExt, Stream, StreamExt,
|
||||
join, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle,
|
||||
@@ -32,7 +30,8 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -241,12 +240,13 @@ impl InlineAssistant {
|
||||
};
|
||||
codegen_ranges.push(start..end);
|
||||
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
@@ -754,33 +754,6 @@ impl InlineAssistant {
|
||||
|
||||
pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
|
||||
if let Some(assist) = self.assists.get(&assist_id) {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.map(|language| language.name())
|
||||
});
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let assist_group_id = assist.group_id;
|
||||
if self.assist_groups[&assist_group_id].linked {
|
||||
for assist_id in self.unlink_assist_group(assist_group_id, cx) {
|
||||
@@ -815,12 +788,45 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||
let message_id = active_alternative.read(cx).message_id.clone();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||
.map(|language| language.name())
|
||||
});
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
message_id,
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
self.telemetry.clone(),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
);
|
||||
}
|
||||
|
||||
if undo {
|
||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||
} else {
|
||||
let confirmed_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||
self.confirmed_assists
|
||||
.insert(assist_id, confirmed_alternative);
|
||||
self.confirmed_assists.insert(assist_id, active_alternative);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2497,6 +2503,7 @@ pub struct CodegenAlternative {
|
||||
line_operations: Vec<LineOperation>,
|
||||
request: Option<LanguageModelRequest>,
|
||||
elapsed_time: Option<f64>,
|
||||
message_id: Option<String>,
|
||||
}
|
||||
|
||||
enum CodegenStatus {
|
||||
@@ -2555,6 +2562,7 @@ impl CodegenAlternative {
|
||||
buffer: buffer.clone(),
|
||||
old_buffer,
|
||||
edit_position: None,
|
||||
message_id: None,
|
||||
snapshot,
|
||||
last_equal_ranges: Default::default(),
|
||||
transformation_transaction_id: None,
|
||||
@@ -2659,20 +2667,20 @@ impl CodegenAlternative {
|
||||
|
||||
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
||||
|
||||
let api_key = model.api_key(cx);
|
||||
let telemetry_id = model.telemetry_id();
|
||||
let provider_id = model.provider_id();
|
||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
||||
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
||||
self.request = Some(request.clone());
|
||||
|
||||
let chunks = cx
|
||||
.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
|
||||
async move { Ok(chunks.await?.boxed()) }.boxed_local()
|
||||
cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await })
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx);
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2737,7 +2745,8 @@ impl CodegenAlternative {
|
||||
&mut self,
|
||||
model_telemetry_id: String,
|
||||
model_provider_id: String,
|
||||
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
|
||||
model_api_key: Option<String>,
|
||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
@@ -2767,6 +2776,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client().clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
@@ -2782,15 +2792,21 @@ impl CodegenAlternative {
|
||||
let mut edit_start = self.range.start.to_offset(&snapshot);
|
||||
self.generation = cx.spawn(|codegen, mut cx| {
|
||||
async move {
|
||||
let chunks = stream.await;
|
||||
let stream = stream.await;
|
||||
let message_id = stream
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|stream| stream.message_id.clone());
|
||||
let generate = async {
|
||||
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
||||
let executor = cx.background_executor().clone();
|
||||
let message_id = message_id.clone();
|
||||
let line_based_stream_diff: Task<anyhow::Result<()>> =
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(chunks?);
|
||||
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -2886,9 +2902,10 @@ impl CodegenAlternative {
|
||||
|
||||
let error_message =
|
||||
result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
message_id,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
@@ -2896,8 +2913,12 @@ impl CodegenAlternative {
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
});
|
||||
}
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
);
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
@@ -2961,6 +2982,7 @@ impl CodegenAlternative {
|
||||
|
||||
codegen
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.message_id = message_id;
|
||||
this.last_equal_ranges.clear();
|
||||
if let Err(error) = result {
|
||||
this.status = CodegenStatus::Error(error);
|
||||
@@ -3512,15 +3534,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
@@ -3584,15 +3598,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
@@ -3659,15 +3665,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
@@ -3733,16 +3731,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||
let new_text = concat!(
|
||||
"func main() {\n",
|
||||
"\tx := 0\n",
|
||||
@@ -3797,16 +3786,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||
chunks_tx
|
||||
.unbounded_send("let mut x = 0;\nx += 1;".to_string())
|
||||
.unwrap();
|
||||
@@ -3880,6 +3860,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_response_stream(
|
||||
codegen: Model<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<String> {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
None,
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -31,6 +31,7 @@ pub mod now_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod search_command;
|
||||
pub mod streaming_example_command;
|
||||
pub mod symbols_command;
|
||||
pub mod tab_command;
|
||||
pub mod terminal_command;
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
use semantic_index::{FileSummary, SemanticDb};
|
||||
use smol::channel;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{BorrowAppContext, WindowContext};
|
||||
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand {
|
||||
"Automatically infer what context to add".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Wand
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DeltaSlashCommand;
|
||||
@@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Diff
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
"Insert diagnostics".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::XCircle
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert file".into()
|
||||
"Insert file and/or directory".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
@@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
true
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::File
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
|
||||
@@ -24,7 +24,8 @@ use std::{
|
||||
ops::DerefMut,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{BorrowAppContext as _, IconName};
|
||||
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ProjectSlashCommand {
|
||||
@@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
"Generate a semantic search based on context".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
|
||||
"Insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Library
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
|
||||
"Search your project semantically".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
136
crates/assistant/src/slash_command/streaming_example_command.rs
Normal file
136
crates/assistant/src/slash_command/streaming_example_command.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use feature_flags::FeatureFlag;
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use smol::stream::StreamExt;
|
||||
use smol::Timer;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct StreamingExampleSlashCommandFeatureFlag;
|
||||
|
||||
impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag {
|
||||
const NAME: &'static str = "streaming-example-slash-command";
|
||||
}
|
||||
|
||||
pub(crate) struct StreamingExampleSlashCommand;
|
||||
|
||||
impl SlashCommand for StreamingExampleSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"streaming-example".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"An example slash command that showcases streaming.".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileRust,
|
||||
label: "Section 1".into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "Hello".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::FileRust,
|
||||
label: "Section 2".into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "World".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
|
||||
for n in 1..=10 {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||
icon: IconName::StarFilled,
|
||||
label: format!("Section {n}").into(),
|
||||
metadata: None,
|
||||
}))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "lorem ipsum ".repeat(n).trim().into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
events_tx
|
||||
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
text: "\n".into(),
|
||||
run_commands_in_text: false,
|
||||
},
|
||||
)))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Task::ready(Ok(events_rx.boxed()))
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
"Insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ListTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{ActiveTheme, WindowContext};
|
||||
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
|
||||
"Insert open tabs (active tab by default)".to_owned()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use gpui::AnyElement;
|
||||
use gpui::DismissEvent;
|
||||
use gpui::WeakView;
|
||||
use picker::PickerEditorPosition;
|
||||
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
|
||||
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
|
||||
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use crate::QuoteSelection;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
@@ -27,6 +21,7 @@ struct SlashCommandInfo {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
args: Option<SharedString>,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -37,6 +32,7 @@ enum SlashCommandEntry {
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
},
|
||||
QuoteButton,
|
||||
}
|
||||
|
||||
impl AsRef<str> for SlashCommandEntry {
|
||||
@@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
|
||||
match self {
|
||||
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
||||
| SlashCommandEntry::Advert { name, .. } => name,
|
||||
SlashCommandEntry::QuoteButton => "Quote Selection",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
if let SlashCommandEntry::Info(info) = command {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
|
||||
on_confirm(cx);
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::QuoteButton => {
|
||||
cx.dispatch_action(Box::new(QuoteSelection));
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
@@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.min_w(px(250.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(div().font_buffer(cx).child({
|
||||
let mut label = format!("/{}", info.name);
|
||||
if let Some(args) =
|
||||
info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(info.icon).size(IconSize::XSmall))
|
||||
.child(div().font_buffer(cx).child({
|
||||
let mut label = format!("{}", info.name);
|
||||
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
SlashCommandEntry::QuoteButton => {
|
||||
let focus = cx.focus_handle();
|
||||
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
|
||||
.child(
|
||||
div().font_buffer(cx).child(
|
||||
Label::new("selection").size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Insert editor selection")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.children(key_binding.map(|kb| kb.render(cx))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
SlashCommandEntry::Advert { renderer, .. } => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
name: command_name.into(),
|
||||
description: menu_text,
|
||||
args,
|
||||
icon: command.icon(),
|
||||
}))
|
||||
})
|
||||
.chain([SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().font_buffer(cx).child(
|
||||
Label::new("create-your-command").size(LabelSize::Small),
|
||||
))
|
||||
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
Label::new("Learn how to create a custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
.chain([
|
||||
SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
||||
.child(
|
||||
div().font_buffer(cx).child(
|
||||
Label::new("create-your-command")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Create your custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
}])
|
||||
SlashCommandEntry::QuoteButton,
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = SlashCommandDelegate {
|
||||
|
||||
@@ -17,7 +17,8 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -306,6 +307,33 @@ impl TerminalInlineAssistant {
|
||||
this.focus_handle(cx).focus(cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
let codegen = assist.codegen.read(cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id: codegen.message_id.clone(),
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: None,
|
||||
},
|
||||
codegen.telemetry.clone(),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
&executor,
|
||||
);
|
||||
}
|
||||
|
||||
assist.codegen.update(cx, |codegen, cx| {
|
||||
if undo {
|
||||
codegen.undo(cx);
|
||||
@@ -1016,6 +1044,7 @@ pub struct Codegen {
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
terminal: Model<Terminal>,
|
||||
generation: Task<()>,
|
||||
message_id: Option<String>,
|
||||
transaction: Option<TerminalTransaction>,
|
||||
}
|
||||
|
||||
@@ -1026,6 +1055,7 @@ impl Codegen {
|
||||
telemetry,
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
message_id: None,
|
||||
transaction: None,
|
||||
}
|
||||
}
|
||||
@@ -1035,6 +1065,8 @@ impl Codegen {
|
||||
return;
|
||||
};
|
||||
|
||||
let model_api_key = model.api_key(cx);
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
@@ -1043,44 +1075,62 @@ impl Codegen {
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
let generate = async {
|
||||
let message_id = response
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|response| response.message_id.clone());
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
|
||||
let task = cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut chunks = response?;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
let task = cx.background_executor().spawn({
|
||||
let message_id = message_id.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let task = async {
|
||||
let mut chunks = response?.stream;
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
hunks_tx.send(chunk).await?;
|
||||
}
|
||||
let chunk = chunk?;
|
||||
hunks_tx.send(chunk).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = task.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
report_assistant_event(
|
||||
AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: None,
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
);
|
||||
|
||||
result?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = task.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: None,
|
||||
});
|
||||
}
|
||||
|
||||
result?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.message_id = message_id;
|
||||
})?;
|
||||
|
||||
while let Some(hunk) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = &mut this.transaction {
|
||||
|
||||
@@ -62,6 +62,9 @@ pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent
|
||||
|
||||
pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Slash
|
||||
}
|
||||
fn label(&self, _cx: &AppContext) -> CodeLabel {
|
||||
CodeLabel::plain(self.name(), None)
|
||||
}
|
||||
|
||||
@@ -686,6 +686,12 @@ async fn download_remote_server_binary(
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"failed to download remote server release: {:?}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
smol::io::copy(response.body_mut(), &mut temp_file).await?;
|
||||
smol::fs::rename(&temp, &target_path).await?;
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ path = "src/call.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
no-webrtc = ["live_kit_client/no-webrtc"]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
|
||||
@@ -341,6 +341,13 @@ impl Telemetry {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
let state = self.state.lock();
|
||||
let enabled = state.settings.metrics;
|
||||
drop(state);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
self: &Arc<Self>,
|
||||
metrics_id: Option<String>,
|
||||
|
||||
@@ -986,6 +986,7 @@ fn editor_blocks(
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
block_id,
|
||||
selected: false,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
});
|
||||
let element = element.downcast_mut::<Stateful<Div>>().unwrap();
|
||||
|
||||
@@ -80,6 +80,8 @@ pub struct ConfirmCodeAction {
|
||||
pub struct ToggleComments {
|
||||
#[serde(default)]
|
||||
pub advance_downwards: bool,
|
||||
#[serde(default)]
|
||||
pub ignore_indent: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
@@ -157,6 +159,13 @@ pub struct DeleteToPreviousWordStart {
|
||||
pub struct FoldAtLevel {
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct SpawnNearestTask {
|
||||
#[serde(default)]
|
||||
pub reveal: task::RevealStrategy,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -182,6 +191,7 @@ impl_actions!(
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
SelectUpByLines,
|
||||
SpawnNearestTask,
|
||||
ShowCompletions,
|
||||
ToggleCodeActions,
|
||||
ToggleComments,
|
||||
|
||||
@@ -660,7 +660,7 @@ impl DisplaySnapshot {
|
||||
new_start..new_end
|
||||
}
|
||||
|
||||
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
@@ -669,7 +669,7 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
|
||||
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
self.inlay_snapshot
|
||||
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
@@ -702,7 +702,7 @@ impl DisplaySnapshot {
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
fold_point.to_inlay_point(&self.fold_snapshot)
|
||||
@@ -710,7 +710,7 @@ impl DisplaySnapshot {
|
||||
|
||||
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
self.tab_snapshot.to_fold_point(tab_point, bias).0
|
||||
}
|
||||
@@ -1001,7 +1001,7 @@ impl DisplaySnapshot {
|
||||
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
|
||||
let wrap_row = self
|
||||
.block_snapshot
|
||||
.to_wrap_point(BlockPoint::new(display_row.0, 0))
|
||||
.to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
|
||||
.row();
|
||||
self.wrap_snapshot.soft_wrap_indent(wrap_row)
|
||||
}
|
||||
@@ -1233,7 +1233,7 @@ impl DisplayPoint {
|
||||
}
|
||||
|
||||
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
|
||||
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
|
||||
let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias);
|
||||
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
|
||||
@@ -2059,6 +2059,112 @@ pub mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_point_translation_with_replace_blocks(cx: &mut gpui::TestAppContext) {
|
||||
cx.background_executor
|
||||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let map = cx.new_model(|cx| {
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
font("Courier"),
|
||||
px(16.0),
|
||||
None,
|
||||
true,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| {
|
||||
map.insert_blocks(
|
||||
[BlockProperties {
|
||||
placement: BlockPlacement::Replace(
|
||||
buffer_snapshot.anchor_before(Point::new(1, 2))
|
||||
..buffer_snapshot.anchor_after(Point::new(2, 3)),
|
||||
),
|
||||
height: 4,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
priority: 0,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
map.snapshot(cx)
|
||||
});
|
||||
|
||||
assert_eq!(snapshot.text(), "abcde\n\n\n\n\npqrst");
|
||||
|
||||
let point_to_display_points = [
|
||||
(Point::new(1, 0), DisplayPoint::new(DisplayRow(1), 0)),
|
||||
(Point::new(2, 0), DisplayPoint::new(DisplayRow(1), 0)),
|
||||
(Point::new(3, 0), DisplayPoint::new(DisplayRow(5), 0)),
|
||||
];
|
||||
for (buffer_point, display_point) in point_to_display_points {
|
||||
assert_eq!(
|
||||
snapshot.point_to_display_point(buffer_point, Bias::Left),
|
||||
display_point,
|
||||
"point_to_display_point({:?}, Bias::Left)",
|
||||
buffer_point
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.point_to_display_point(buffer_point, Bias::Right),
|
||||
display_point,
|
||||
"point_to_display_point({:?}, Bias::Right)",
|
||||
buffer_point
|
||||
);
|
||||
}
|
||||
|
||||
let display_points_to_points = [
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(1), 0),
|
||||
Point::new(1, 0),
|
||||
Point::new(2, 5),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(2), 0),
|
||||
Point::new(1, 0),
|
||||
Point::new(2, 5),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(3), 0),
|
||||
Point::new(1, 0),
|
||||
Point::new(2, 5),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(4), 0),
|
||||
Point::new(1, 0),
|
||||
Point::new(2, 5),
|
||||
),
|
||||
(
|
||||
DisplayPoint::new(DisplayRow(5), 0),
|
||||
Point::new(3, 0),
|
||||
Point::new(3, 0),
|
||||
),
|
||||
];
|
||||
for (display_point, left_buffer_point, right_buffer_point) in display_points_to_points {
|
||||
assert_eq!(
|
||||
snapshot.display_point_to_point(display_point, Bias::Left),
|
||||
left_buffer_point,
|
||||
"display_point_to_point({:?}, Bias::Left)",
|
||||
display_point
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.display_point_to_point(display_point, Bias::Right),
|
||||
right_buffer_point,
|
||||
"display_point_to_point({:?}, Bias::Right)",
|
||||
display_point
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// todo(linux) fails due to pixel differences in text rendering
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
|
||||
@@ -265,6 +265,7 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub block_id: BlockId,
|
||||
pub selected: bool,
|
||||
pub editor_style: &'b EditorStyle,
|
||||
}
|
||||
|
||||
@@ -1311,7 +1312,6 @@ impl BlockSnapshot {
|
||||
let (output_start_row, input_start_row) = cursor.start();
|
||||
let (output_end_row, input_end_row) = cursor.end(&());
|
||||
let output_start = Point::new(output_start_row.0, 0);
|
||||
let output_end = Point::new(output_end_row.0, 0);
|
||||
let input_start = Point::new(input_start_row.0, 0);
|
||||
let input_end = Point::new(input_end_row.0, 0);
|
||||
|
||||
@@ -1319,10 +1319,10 @@ impl BlockSnapshot {
|
||||
Some(Block::Custom(block))
|
||||
if matches!(block.placement, BlockPlacement::Replace(_)) =>
|
||||
{
|
||||
if bias == Bias::Left {
|
||||
if ((bias == Bias::Left || search_left) && output_start <= point.0)
|
||||
|| (!search_left && output_start >= point.0)
|
||||
{
|
||||
return BlockPoint(output_start);
|
||||
} else {
|
||||
return BlockPoint(Point::new(output_end.row - 1, 0));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -1364,12 +1364,7 @@ impl BlockSnapshot {
|
||||
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
|
||||
if let Some(transform) = cursor.item() {
|
||||
if transform.block.is_some() {
|
||||
let wrap_start = WrapPoint::new(cursor.start().0 .0, 0);
|
||||
if wrap_start == wrap_point {
|
||||
BlockPoint::new(cursor.start().1 .0, 0)
|
||||
} else {
|
||||
BlockPoint::new(cursor.end(&()).1 .0 - 1, 0)
|
||||
}
|
||||
BlockPoint::new(cursor.start().1 .0, 0)
|
||||
} else {
|
||||
let (input_start_row, output_start_row) = cursor.start();
|
||||
let input_start = Point::new(input_start_row.0, 0);
|
||||
@@ -1382,7 +1377,7 @@ impl BlockSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
|
||||
pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
|
||||
if let Some(transform) = cursor.item() {
|
||||
@@ -1391,7 +1386,9 @@ impl BlockSnapshot {
|
||||
if block.place_below() {
|
||||
let wrap_row = cursor.start().1 .0 - 1;
|
||||
WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
|
||||
} else if block.place_above() || block_point.row == cursor.start().0 .0 {
|
||||
} else if block.place_above() {
|
||||
WrapPoint::new(cursor.start().1 .0, 0)
|
||||
} else if bias == Bias::Left {
|
||||
WrapPoint::new(cursor.start().1 .0, 0)
|
||||
} else {
|
||||
let wrap_row = cursor.end(&()).1 .0 - 1;
|
||||
@@ -1766,19 +1763,19 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snapshot.to_wrap_point(BlockPoint::new(0, 3)),
|
||||
snapshot.to_wrap_point(BlockPoint::new(0, 3), Bias::Left),
|
||||
WrapPoint::new(0, 3)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.to_wrap_point(BlockPoint::new(1, 0)),
|
||||
snapshot.to_wrap_point(BlockPoint::new(1, 0), Bias::Left),
|
||||
WrapPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.to_wrap_point(BlockPoint::new(3, 0)),
|
||||
snapshot.to_wrap_point(BlockPoint::new(3, 0), Bias::Left),
|
||||
WrapPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.to_wrap_point(BlockPoint::new(7, 0)),
|
||||
snapshot.to_wrap_point(BlockPoint::new(7, 0), Bias::Left),
|
||||
WrapPoint::new(3, 3)
|
||||
);
|
||||
|
||||
@@ -2616,10 +2613,15 @@ mod tests {
|
||||
|
||||
// Ensure that conversion between block points and wrap points is stable.
|
||||
for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
|
||||
let original_wrap_point = WrapPoint::new(row, 0);
|
||||
let block_point = blocks_snapshot.to_block_point(original_wrap_point);
|
||||
let wrap_point = blocks_snapshot.to_wrap_point(block_point);
|
||||
assert_eq!(blocks_snapshot.to_block_point(wrap_point), block_point);
|
||||
let wrap_point = WrapPoint::new(row, 0);
|
||||
let block_point = blocks_snapshot.to_block_point(wrap_point);
|
||||
let left_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Left);
|
||||
let right_wrap_point = blocks_snapshot.to_wrap_point(block_point, Bias::Right);
|
||||
assert_eq!(blocks_snapshot.to_block_point(left_wrap_point), block_point);
|
||||
assert_eq!(
|
||||
blocks_snapshot.to_block_point(right_wrap_point),
|
||||
block_point
|
||||
);
|
||||
}
|
||||
|
||||
let mut block_point = BlockPoint::new(0, 0);
|
||||
@@ -2627,10 +2629,12 @@ mod tests {
|
||||
let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
|
||||
let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
|
||||
assert_eq!(
|
||||
blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
|
||||
blocks_snapshot
|
||||
.to_block_point(blocks_snapshot.to_wrap_point(left_point, Bias::Left)),
|
||||
left_point,
|
||||
"wrap point: {:?}",
|
||||
blocks_snapshot.to_wrap_point(left_point)
|
||||
"block point: {:?}, wrap point: {:?}",
|
||||
block_point,
|
||||
blocks_snapshot.to_wrap_point(left_point, Bias::Left)
|
||||
);
|
||||
assert_eq!(
|
||||
left_buffer_point,
|
||||
@@ -2642,10 +2646,12 @@ mod tests {
|
||||
let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
|
||||
let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
|
||||
assert_eq!(
|
||||
blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
|
||||
blocks_snapshot
|
||||
.to_block_point(blocks_snapshot.to_wrap_point(right_point, Bias::Right)),
|
||||
right_point,
|
||||
"wrap point: {:?}",
|
||||
blocks_snapshot.to_wrap_point(right_point)
|
||||
"block point: {:?}, wrap point: {:?}",
|
||||
block_point,
|
||||
blocks_snapshot.to_wrap_point(right_point, Bias::Right)
|
||||
);
|
||||
assert_eq!(
|
||||
right_buffer_point,
|
||||
@@ -2681,7 +2687,8 @@ mod tests {
|
||||
|
||||
impl BlockSnapshot {
|
||||
fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
|
||||
self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
|
||||
self.wrap_snapshot
|
||||
.to_point(self.to_wrap_point(point, bias), bias)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,9 @@ use project::{
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use selections_collection::{
|
||||
resolve_selections, MutableSelectionsCollection, SelectionsCollection,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
@@ -505,6 +507,19 @@ struct RunnableTasks {
|
||||
context_range: Range<BufferOffset>,
|
||||
}
|
||||
|
||||
impl RunnableTasks {
|
||||
fn resolve<'a>(
|
||||
&'a self,
|
||||
cx: &'a task::TaskContext,
|
||||
) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
|
||||
self.templates.iter().filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), cx)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
@@ -3489,8 +3504,8 @@ impl Editor {
|
||||
}
|
||||
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
||||
let new_selection_deltas = new_selections.iter().map(|e| e.1);
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
|
||||
let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let new_selections = resolve_selections::<usize, _>(new_anchor_selections, &map)
|
||||
.zip(new_selection_deltas)
|
||||
.map(|(selection, delta)| Selection {
|
||||
id: selection.id,
|
||||
@@ -3503,18 +3518,20 @@ impl Editor {
|
||||
|
||||
let mut i = 0;
|
||||
for (position, delta, selection_id, pair) in new_autoclose_regions {
|
||||
let position = position.to_offset(&snapshot) + delta;
|
||||
let start = snapshot.anchor_before(position);
|
||||
let end = snapshot.anchor_after(position);
|
||||
let position = position.to_offset(&map.buffer_snapshot) + delta;
|
||||
let start = map.buffer_snapshot.anchor_before(position);
|
||||
let end = map.buffer_snapshot.anchor_after(position);
|
||||
while let Some(existing_state) = this.autoclose_regions.get(i) {
|
||||
match existing_state.range.start.cmp(&start, &snapshot) {
|
||||
match existing_state.range.start.cmp(&start, &map.buffer_snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Greater => break,
|
||||
Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Equal => break,
|
||||
Ordering::Greater => break,
|
||||
},
|
||||
Ordering::Equal => {
|
||||
match end.cmp(&existing_state.range.end, &map.buffer_snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Equal => break,
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.autoclose_regions.insert(
|
||||
@@ -3527,7 +3544,6 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
let had_active_inline_completion = this.has_active_inline_completion(cx);
|
||||
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
|
||||
s.select(new_selections)
|
||||
@@ -4043,7 +4059,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
(selection.clone(), enclosing)
|
||||
(selection, enclosing)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4741,29 +4757,7 @@ impl Editor {
|
||||
.as_ref()
|
||||
.zip(editor.project.clone())
|
||||
.map(|(tasks, project)| {
|
||||
let position = Point::new(buffer_row, tasks.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
let location = Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range_start..range_start,
|
||||
};
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut captured_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.extra_variables.clone() {
|
||||
captured_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_store().update(cx, |task_store, cx| {
|
||||
task_store.task_context_for_location(
|
||||
captured_task_variables,
|
||||
location,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|editor, mut cx| async move {
|
||||
@@ -4774,15 +4768,7 @@ impl Editor {
|
||||
let resolved_tasks =
|
||||
tasks.zip(task_context).map(|(tasks, task_context)| {
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot.buffer_snapshot.anchor_before(Point::new(
|
||||
multibuffer_point.row,
|
||||
tasks.column,
|
||||
@@ -5714,6 +5700,132 @@ impl Editor {
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Model<Project>,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_row: u32,
|
||||
tasks: &Arc<RunnableTasks>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Option<task::TaskContext>> {
|
||||
let position = Point::new(buffer_row, tasks.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
let location = Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range_start..range_start,
|
||||
};
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut captured_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.extra_variables.clone() {
|
||||
captured_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_store().update(cx, |task_store, cx| {
|
||||
task_store.task_context_for_location(captured_task_variables, location, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext<Self>) {
|
||||
let Some((workspace, _)) = self.workspace.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(project) = self.project.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to find a closest, enclosing node using tree-sitter that has a
|
||||
// task
|
||||
let Some((buffer, buffer_row, tasks)) = self
|
||||
.find_enclosing_node_task(cx)
|
||||
// Or find the task that's closest in row-distance.
|
||||
.or_else(|| self.find_closest_task(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let reveal_strategy = action.reveal;
|
||||
let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let context = task_context.await?;
|
||||
let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
|
||||
|
||||
let resolved = resolved_task.resolved.as_mut()?;
|
||||
resolved.reveal = reveal_strategy;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace::tasks::schedule_resolved_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
resolved_task,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn find_closest_task(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
|
||||
let cursor_row = self.selections.newest_adjusted(cx).head().row;
|
||||
|
||||
let ((buffer_id, row), tasks) = self
|
||||
.tasks
|
||||
.iter()
|
||||
.min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
|
||||
|
||||
let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
|
||||
let tasks = Arc::new(tasks.to_owned());
|
||||
Some((buffer, *row, tasks))
|
||||
}
|
||||
|
||||
fn find_enclosing_node_task(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let offset = self.selections.newest::<usize>(cx).head();
|
||||
let excerpt = snapshot.excerpt_containing(offset..offset)?;
|
||||
let buffer_id = excerpt.buffer().remote_id();
|
||||
|
||||
let layer = excerpt.buffer().syntax_layer_at(offset)?;
|
||||
let mut cursor = layer.node().walk();
|
||||
|
||||
while cursor.goto_first_child_for_byte(offset).is_some() {
|
||||
if cursor.node().end_byte() == offset {
|
||||
cursor.goto_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
// Ascend to the smallest ancestor that contains the range and has a task.
|
||||
loop {
|
||||
let node = cursor.node();
|
||||
let node_range = node.byte_range();
|
||||
let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
|
||||
|
||||
// Check if this node contains our offset
|
||||
if node_range.start <= offset && node_range.end >= offset {
|
||||
// If it contains offset, check for task
|
||||
if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
|
||||
let buffer = self.buffer.read(cx).buffer(buffer_id)?;
|
||||
return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
|
||||
}
|
||||
}
|
||||
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -9011,14 +9123,22 @@ impl Editor {
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let mut suffixes_inserted = Vec::new();
|
||||
let ignore_indent = action.ignore_indent;
|
||||
|
||||
fn comment_prefix_range(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
row: MultiBufferRow,
|
||||
comment_prefix: &str,
|
||||
comment_prefix_whitespace: &str,
|
||||
ignore_indent: bool,
|
||||
) -> Range<Point> {
|
||||
let start = Point::new(row.0, snapshot.indent_size_for_line(row).len);
|
||||
let indent_size = if ignore_indent {
|
||||
0
|
||||
} else {
|
||||
snapshot.indent_size_for_line(row).len
|
||||
};
|
||||
|
||||
let start = Point::new(row.0, indent_size);
|
||||
|
||||
let mut line_bytes = snapshot
|
||||
.bytes_in_range(start..snapshot.max_point())
|
||||
@@ -9114,7 +9234,16 @@ impl Editor {
|
||||
}
|
||||
|
||||
// If the language has line comments, toggle those.
|
||||
let full_comment_prefixes = language.line_comment_prefixes();
|
||||
let mut full_comment_prefixes = language.line_comment_prefixes().to_vec();
|
||||
|
||||
// If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes
|
||||
if ignore_indent {
|
||||
full_comment_prefixes = full_comment_prefixes
|
||||
.into_iter()
|
||||
.map(|s| Arc::from(s.trim_end()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !full_comment_prefixes.is_empty() {
|
||||
let first_prefix = full_comment_prefixes
|
||||
.first()
|
||||
@@ -9141,6 +9270,7 @@ impl Editor {
|
||||
row,
|
||||
&prefix[..trimmed_prefix_len],
|
||||
&prefix[trimmed_prefix_len..],
|
||||
ignore_indent,
|
||||
)
|
||||
})
|
||||
.max_by_key(|range| range.end.column - range.start.column)
|
||||
@@ -9181,6 +9311,7 @@ impl Editor {
|
||||
start_row,
|
||||
comment_prefix,
|
||||
comment_prefix_whitespace,
|
||||
ignore_indent,
|
||||
);
|
||||
let suffix_range = comment_suffix_range(
|
||||
snapshot.deref(),
|
||||
|
||||
@@ -3989,6 +3989,76 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(
|
||||
&"
|
||||
ˇzero
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// Create a four-line block that replaces three lines of text.
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let placement = BlockPlacement::Replace(
|
||||
snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
|
||||
);
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
placement,
|
||||
height: 4,
|
||||
style: BlockStyle::Sticky,
|
||||
render: Box::new(|_| gpui::div().into_any_element()),
|
||||
priority: 0,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Move down so that the cursor touches the block.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
zero
|
||||
«one
|
||||
two
|
||||
threeˇ»
|
||||
four
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// Move down past the block.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
zero
|
||||
one
|
||||
two
|
||||
three
|
||||
ˇfour
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_transpose(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -4182,7 +4252,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
// et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
|
||||
// dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
|
||||
// viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
|
||||
// porttitor id. Aliquam id accumsan eros.ˇˇˇˇ
|
||||
// porttitor id. Aliquam id accumsan eros.ˇ
|
||||
"};
|
||||
|
||||
cx.set_state(unwrapped_text);
|
||||
@@ -4212,7 +4282,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
let wrapped_text = indoc! {"
|
||||
// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
|
||||
// purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
|
||||
// auctor, eu lacinia sapien scelerisque.ˇˇ
|
||||
// auctor, eu lacinia sapien scelerisque.ˇ
|
||||
//
|
||||
// Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
|
||||
// tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
|
||||
@@ -4220,7 +4290,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
// molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
|
||||
// nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
|
||||
// porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
|
||||
// vulputate turpis porttitor id. Aliquam id accumsan eros.ˇˇ
|
||||
// vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ
|
||||
"};
|
||||
|
||||
cx.set_state(unwrapped_text);
|
||||
@@ -8533,6 +8603,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
let toggle_comments = &ToggleComments {
|
||||
advance_downwards: false,
|
||||
ignore_indent: true,
|
||||
};
|
||||
|
||||
// If multiple selections intersect a line, the line is only toggled once.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
// «b();
|
||||
// c();
|
||||
// ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
«b();
|
||||
c();
|
||||
ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
// The comment prefix is inserted at the beginning of each line
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «b();
|
||||
// c();
|
||||
// ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection ends at the beginning of a line, that line is not toggled.
|
||||
cx.set_selections_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
// «c();
|
||||
ˇ»// d();
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
«c();
|
||||
ˇ»// d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span a single line and is empty, the line is toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
//ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span multiple lines, empty lines are not toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
«a();
|
||||
|
||||
c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «a();
|
||||
|
||||
// c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection includes multiple comment prefixes, all lines are uncommented.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
// «a();
|
||||
/// b();
|
||||
//! c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
«a();
|
||||
b();
|
||||
c();ˇ»
|
||||
}
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -8554,6 +8749,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
||||
|
||||
let toggle_comments = &ToggleComments {
|
||||
advance_downwards: true,
|
||||
ignore_indent: false,
|
||||
};
|
||||
|
||||
// Single cursor on one line -> advance
|
||||
@@ -13204,6 +13400,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
|
||||
let text = r#"
|
||||
#[cfg(test)]
|
||||
mod tests() {
|
||||
#[test]
|
||||
fn runnable_1() {
|
||||
let a = 1;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runnable_2() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.tasks.insert(
|
||||
(buffer.read(cx).remote_id(), 3),
|
||||
RunnableTasks {
|
||||
templates: vec![],
|
||||
offset: MultiBufferOffset(43),
|
||||
column: 0,
|
||||
extra_variables: HashMap::default(),
|
||||
context_range: BufferOffset(43)..BufferOffset(85),
|
||||
},
|
||||
);
|
||||
editor.tasks.insert(
|
||||
(buffer.read(cx).remote_id(), 8),
|
||||
RunnableTasks {
|
||||
templates: vec![],
|
||||
offset: MultiBufferOffset(86),
|
||||
column: 0,
|
||||
extra_variables: HashMap::default(),
|
||||
context_range: BufferOffset(86)..BufferOffset(191),
|
||||
},
|
||||
);
|
||||
|
||||
// Test finding task when cursor is inside function body
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||
});
|
||||
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
|
||||
assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
|
||||
|
||||
// Test finding task when cursor is on function name
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
|
||||
});
|
||||
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
|
||||
assert_eq!(row, 8, "Should find task when cursor is on function name");
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
|
||||
use gpui::Subscription;
|
||||
use gpui::{
|
||||
@@ -451,7 +451,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::apply_selected_diff_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal);
|
||||
register_action(view, cx, Editor::toggle_breakpoint);
|
||||
register_action(view, cx, Editor::reload_file)
|
||||
register_action(view, cx, Editor::reload_file);
|
||||
register_action(view, cx, Editor::spawn_nearest_task);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
|
||||
@@ -821,10 +822,12 @@ impl EditorElement {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_selections(
|
||||
&self,
|
||||
start_anchor: Anchor,
|
||||
end_anchor: Anchor,
|
||||
local_selections: &[Selection<Point>],
|
||||
snapshot: &EditorSnapshot,
|
||||
start_row: DisplayRow,
|
||||
end_row: DisplayRow,
|
||||
@@ -839,13 +842,9 @@ impl EditorElement {
|
||||
let mut newest_selection_head = None;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if editor.show_local_selections {
|
||||
let mut local_selections: Vec<Selection<Point>> = editor
|
||||
.selections
|
||||
.disjoint_in_range(start_anchor..end_anchor, cx);
|
||||
local_selections.extend(editor.selections.pending(cx));
|
||||
let mut layouts = Vec::new();
|
||||
let newest = editor.selections.newest(cx);
|
||||
for selection in local_selections.drain(..) {
|
||||
for selection in local_selections.iter().cloned() {
|
||||
let is_empty = selection.start == selection.end;
|
||||
let is_newest = selection == newest;
|
||||
|
||||
@@ -1008,6 +1007,7 @@ impl EditorElement {
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
selections: &[(PlayerColor, Vec<SelectionLayout>)],
|
||||
block_start_rows: &HashSet<DisplayRow>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
text_hitbox: &Hitbox,
|
||||
@@ -1027,7 +1027,10 @@ impl EditorElement {
|
||||
let cursor_position = selection.head;
|
||||
|
||||
let in_range = visible_display_row_range.contains(&cursor_position.row());
|
||||
if (selection.is_local && !editor.show_local_cursors(cx)) || !in_range {
|
||||
if (selection.is_local && !editor.show_local_cursors(cx))
|
||||
|| !in_range
|
||||
|| block_start_rows.contains(&cursor_position.row())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2154,14 +2157,14 @@ impl EditorElement {
|
||||
editor_width: Pixels,
|
||||
scroll_width: &mut Pixels,
|
||||
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
||||
selections: &[Selection<Point>],
|
||||
cx: &mut WindowContext,
|
||||
) -> (AnyElement, Size<Pixels>) {
|
||||
let mut element = match block {
|
||||
Block::Custom(block) => {
|
||||
let align_to = block
|
||||
.start()
|
||||
.to_point(&snapshot.buffer_snapshot)
|
||||
.to_display_point(snapshot);
|
||||
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
|
||||
let block_end = block.end().to_point(&snapshot.buffer_snapshot);
|
||||
let align_to = block_start.to_display_point(snapshot);
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&align_to.row()) {
|
||||
line_layouts[align_to.row().minus(rows.start) as usize]
|
||||
@@ -2171,6 +2174,18 @@ impl EditorElement {
|
||||
.x_for_index(align_to.column() as usize)
|
||||
};
|
||||
|
||||
let selected = selections
|
||||
.binary_search_by(|selection| {
|
||||
if selection.end <= block_start {
|
||||
Ordering::Less
|
||||
} else if selection.start >= block_end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.child(block.render(&mut BlockContext {
|
||||
@@ -2180,6 +2195,7 @@ impl EditorElement {
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
selected,
|
||||
max_width: text_hitbox.size.width.max(*scroll_width),
|
||||
editor_style: &self.style,
|
||||
}))
|
||||
@@ -2517,6 +2533,7 @@ impl EditorElement {
|
||||
text_x: Pixels,
|
||||
line_height: Pixels,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
selections: &[Selection<Point>],
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
@@ -2553,6 +2570,7 @@ impl EditorElement {
|
||||
editor_width,
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
cx,
|
||||
);
|
||||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||||
@@ -2597,6 +2615,7 @@ impl EditorElement {
|
||||
editor_width,
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -2642,6 +2661,7 @@ impl EditorElement {
|
||||
editor_width,
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -2670,6 +2690,7 @@ impl EditorElement {
|
||||
fn layout_blocks(
|
||||
&self,
|
||||
blocks: &mut Vec<BlockLayout>,
|
||||
block_starts: &mut HashSet<DisplayRow>,
|
||||
hitbox: &Hitbox,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
@@ -2677,6 +2698,7 @@ impl EditorElement {
|
||||
) {
|
||||
for block in blocks {
|
||||
let mut origin = if let Some(row) = block.row {
|
||||
block_starts.insert(row);
|
||||
hitbox.origin
|
||||
+ point(
|
||||
Pixels::ZERO,
|
||||
@@ -5195,9 +5217,19 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let local_selections: Vec<Selection<Point>> =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut selections = editor
|
||||
.selections
|
||||
.disjoint_in_range(start_anchor..end_anchor, cx);
|
||||
selections.extend(editor.selections.pending(cx));
|
||||
selections
|
||||
});
|
||||
|
||||
let (selections, active_rows, newest_selection_head) = self.layout_selections(
|
||||
start_anchor,
|
||||
end_anchor,
|
||||
&local_selections,
|
||||
&snapshot,
|
||||
start_row,
|
||||
end_row,
|
||||
@@ -5290,6 +5322,7 @@ impl Element for EditorElement {
|
||||
gutter_dimensions.full_width(),
|
||||
line_height,
|
||||
&line_layouts,
|
||||
&local_selections,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -5429,9 +5462,11 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut block_start_rows = HashSet::default();
|
||||
cx.with_element_namespace("blocks", |cx| {
|
||||
self.layout_blocks(
|
||||
&mut blocks,
|
||||
&mut block_start_rows,
|
||||
&hitbox,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
@@ -5448,6 +5483,7 @@ impl Element for EditorElement {
|
||||
let visible_cursors = self.layout_visible_cursors(
|
||||
&snapshot,
|
||||
&selections,
|
||||
&block_start_rows,
|
||||
start_row..end_row,
|
||||
&line_layouts,
|
||||
&text_hitbox,
|
||||
|
||||
@@ -1351,6 +1351,61 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
// https://github.com/zed-industries/zed/issues/15498
|
||||
async fn test_info_hover_with_hrs(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
fn fuˇnc(abc def: i32) -> u32 {
|
||||
}
|
||||
"});
|
||||
|
||||
cx.lsp.handle_request::<lsp::request::HoverRequest, _, _>({
|
||||
|_, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc!(
|
||||
r#"
|
||||
### function `errands_data_read`
|
||||
|
||||
---
|
||||
→ `char *`
|
||||
Function to read a file into a string
|
||||
|
||||
---
|
||||
```cpp
|
||||
static char *errands_data_read()
|
||||
```
|
||||
"#
|
||||
)
|
||||
.to_string(),
|
||||
}),
|
||||
range: None,
|
||||
}))
|
||||
}
|
||||
});
|
||||
cx.update_editor(|editor, cx| hover(editor, &Default::default(), cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
let popover = editor.hover_state.info_popovers.first().unwrap();
|
||||
let content = popover.get_rendered_text(cx);
|
||||
|
||||
assert!(content.contains("Function to read a file"));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cell::Ref,
|
||||
iter, mem,
|
||||
cmp, iter, mem,
|
||||
ops::{Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -98,9 +98,9 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Selection<D>> {
|
||||
self.pending_anchor()
|
||||
.as_ref()
|
||||
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
|
||||
let map = self.display_map(cx);
|
||||
let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next();
|
||||
selection
|
||||
}
|
||||
|
||||
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
|
||||
@@ -111,12 +111,10 @@ impl SelectionsCollection {
|
||||
where
|
||||
D: 'a + TextDimension + Ord + Sub<D, Output = D>,
|
||||
{
|
||||
let map = self.display_map(cx);
|
||||
let disjoint_anchors = &self.disjoint;
|
||||
let mut disjoint =
|
||||
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
|
||||
|
||||
let mut disjoint = resolve_selections::<D, _>(disjoint_anchors.iter(), &map).peekable();
|
||||
let mut pending_opt = self.pending::<D>(cx);
|
||||
|
||||
iter::from_fn(move || {
|
||||
if let Some(pending) = pending_opt.as_mut() {
|
||||
while let Some(next_selection) = disjoint.peek() {
|
||||
@@ -199,34 +197,57 @@ impl SelectionsCollection {
|
||||
where
|
||||
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
|
||||
{
|
||||
let buffer = self.buffer(cx);
|
||||
let map = self.display_map(cx);
|
||||
let start_ix = match self
|
||||
.disjoint
|
||||
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
|
||||
.binary_search_by(|probe| probe.end.cmp(&range.start, &map.buffer_snapshot))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let end_ix = match self
|
||||
.disjoint
|
||||
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
|
||||
.binary_search_by(|probe| probe.start.cmp(&range.end, &map.buffer_snapshot))
|
||||
{
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
|
||||
resolve_selections(&self.disjoint[start_ix..end_ix], &map).collect()
|
||||
}
|
||||
|
||||
pub fn all_display(
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
|
||||
let display_map = self.display_map(cx);
|
||||
let selections = self
|
||||
.all::<Point>(cx)
|
||||
.into_iter()
|
||||
.map(|selection| selection.map(|point| point.to_display_point(&display_map)))
|
||||
.collect();
|
||||
(display_map, selections)
|
||||
let map = self.display_map(cx);
|
||||
let disjoint_anchors = &self.disjoint;
|
||||
let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable();
|
||||
let mut pending_opt =
|
||||
resolve_selections_display(self.pending_anchor().as_ref(), &map).next();
|
||||
let selections = iter::from_fn(move || {
|
||||
if let Some(pending) = pending_opt.as_mut() {
|
||||
while let Some(next_selection) = disjoint.peek() {
|
||||
if pending.start <= next_selection.end && pending.end >= next_selection.start {
|
||||
let next_selection = disjoint.next().unwrap();
|
||||
if next_selection.start < pending.start {
|
||||
pending.start = next_selection.start;
|
||||
}
|
||||
if next_selection.end > pending.end {
|
||||
pending.end = next_selection.end;
|
||||
}
|
||||
} else if next_selection.end < pending.start {
|
||||
return disjoint.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pending_opt.take()
|
||||
} else {
|
||||
disjoint.next()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(map, selections)
|
||||
}
|
||||
|
||||
pub fn newest_anchor(&self) -> &Selection<Anchor> {
|
||||
@@ -241,15 +262,18 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
) -> Selection<D> {
|
||||
let buffer = self.buffer(cx);
|
||||
self.newest_anchor().map(|p| p.summary::<D>(&buffer))
|
||||
let map = self.display_map(cx);
|
||||
let selection = resolve_selections([self.newest_anchor()], &map)
|
||||
.next()
|
||||
.unwrap();
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
|
||||
let display_map = self.display_map(cx);
|
||||
let selection = self
|
||||
.newest_anchor()
|
||||
.map(|point| point.to_display_point(&display_map));
|
||||
let map = self.display_map(cx);
|
||||
let selection = resolve_selections_display([self.newest_anchor()], &map)
|
||||
.next()
|
||||
.unwrap();
|
||||
selection
|
||||
}
|
||||
|
||||
@@ -265,8 +289,11 @@ impl SelectionsCollection {
|
||||
&self,
|
||||
cx: &mut AppContext,
|
||||
) -> Selection<D> {
|
||||
let buffer = self.buffer(cx);
|
||||
self.oldest_anchor().map(|p| p.summary::<D>(&buffer))
|
||||
let map = self.display_map(cx);
|
||||
let selection = resolve_selections([self.oldest_anchor()], &map)
|
||||
.next()
|
||||
.unwrap();
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn first_anchor(&self) -> Selection<Anchor> {
|
||||
@@ -538,9 +565,9 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let map = self.display_map();
|
||||
let resolved_selections =
|
||||
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
|
||||
resolve_selections::<usize, _>(&selections, &map).collect::<Vec<_>>();
|
||||
self.select(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -650,20 +677,16 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
) {
|
||||
let mut changed = false;
|
||||
let display_map = self.display_map();
|
||||
let selections = self
|
||||
.collection
|
||||
.all::<Point>(self.cx)
|
||||
let (_, selections) = self.collection.all_display(self.cx);
|
||||
let selections = selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let mut moved_selection =
|
||||
selection.map(|point| point.to_display_point(&display_map));
|
||||
let mut moved_selection = selection.clone();
|
||||
move_selection(&display_map, &mut moved_selection);
|
||||
let moved_selection =
|
||||
moved_selection.map(|display_point| display_point.to_point(&display_map));
|
||||
if selection != moved_selection {
|
||||
changed = true;
|
||||
}
|
||||
moved_selection
|
||||
moved_selection.map(|display_point| display_point.to_point(&display_map))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -804,8 +827,8 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
.collect();
|
||||
|
||||
if !adjusted_disjoint.is_empty() {
|
||||
let resolved_selections =
|
||||
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
|
||||
let map = self.display_map();
|
||||
let resolved_selections = resolve_selections(adjusted_disjoint.iter(), &map).collect();
|
||||
self.select::<usize>(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -849,27 +872,76 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_multiple<'a, D, I>(
|
||||
selections: I,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = Selection<D>>
|
||||
where
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
{
|
||||
fn resolve_selections_display<'a>(
|
||||
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
map: &'a DisplaySnapshot,
|
||||
) -> impl 'a + Iterator<Item = Selection<DisplayPoint>> {
|
||||
let (to_summarize, selections) = selections.into_iter().tee();
|
||||
let mut summaries = snapshot
|
||||
.summaries_for_anchors::<D, _>(
|
||||
to_summarize
|
||||
.flat_map(|s| [&s.start, &s.end])
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
let mut summaries = map
|
||||
.buffer_snapshot
|
||||
.summaries_for_anchors::<Point, _>(to_summarize.flat_map(|s| [&s.start, &s.end]))
|
||||
.into_iter();
|
||||
selections.map(move |s| Selection {
|
||||
id: s.id,
|
||||
start: summaries.next().unwrap(),
|
||||
end: summaries.next().unwrap(),
|
||||
reversed: s.reversed,
|
||||
goal: s.goal,
|
||||
let mut selections = selections
|
||||
.map(move |s| {
|
||||
let start = summaries.next().unwrap();
|
||||
let end = summaries.next().unwrap();
|
||||
|
||||
let display_start = map.point_to_display_point(start, Bias::Left);
|
||||
let display_end = if start == end {
|
||||
map.point_to_display_point(end, Bias::Right)
|
||||
} else {
|
||||
map.point_to_display_point(end, Bias::Left)
|
||||
};
|
||||
|
||||
Selection {
|
||||
id: s.id,
|
||||
start: display_start,
|
||||
end: display_end,
|
||||
reversed: s.reversed,
|
||||
goal: s.goal,
|
||||
}
|
||||
})
|
||||
.peekable();
|
||||
iter::from_fn(move || {
|
||||
let mut selection = selections.next()?;
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if selection.end >= next_selection.start {
|
||||
selection.end = cmp::max(selection.end, next_selection.end);
|
||||
selections.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(selection)
|
||||
})
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_selections<'a, D, I>(
|
||||
selections: I,
|
||||
map: &'a DisplaySnapshot,
|
||||
) -> impl 'a + Iterator<Item = Selection<D>>
|
||||
where
|
||||
D: TextDimension + Clone + Ord + Sub<D, Output = D>,
|
||||
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
{
|
||||
let (to_convert, selections) = resolve_selections_display(selections, map).tee();
|
||||
let mut converted_endpoints =
|
||||
map.buffer_snapshot
|
||||
.dimensions_from_points::<D>(to_convert.flat_map(|s| {
|
||||
let start = map.display_point_to_point(s.start, Bias::Left);
|
||||
let end = map.display_point_to_point(s.end, Bias::Right);
|
||||
[start, end]
|
||||
}));
|
||||
selections.map(move |s| {
|
||||
let start = converted_endpoints.next().unwrap();
|
||||
let end = converted_endpoints.next().unwrap();
|
||||
Selection {
|
||||
id: s.id,
|
||||
start,
|
||||
end,
|
||||
reversed: s.reversed,
|
||||
goal: s.goal,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,59 +9,23 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/extension_store.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
no-webrtc = ["workspace/no-webrtc"]
|
||||
path = "src/extension.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
wasm-encoder.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
wasmtime.workspace = true
|
||||
wit-component.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
50
crates/extension/src/extension.rs
Normal file
50
crates/extension/src/extension.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_manifest;
|
||||
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use semantic_version::SemanticVersion;
|
||||
|
||||
pub use crate::extension_manifest::*;
|
||||
|
||||
pub fn parse_wasm_extension_version(
|
||||
extension_id: &str,
|
||||
wasm_bytes: &[u8],
|
||||
) -> Result<SemanticVersion> {
|
||||
let mut version = None;
|
||||
|
||||
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
|
||||
if let wasmparser::Payload::CustomSection(s) =
|
||||
part.context("error parsing wasm extension")?
|
||||
{
|
||||
if s.name() == "zed:api-version" {
|
||||
version = parse_wasm_extension_version_custom_section(s.data());
|
||||
if version.is_none() {
|
||||
bail!(
|
||||
"extension {} has invalid zed:api-version section: {:?}",
|
||||
extension_id,
|
||||
s.data()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reason we wait until we're done parsing all of the Wasm bytes to return the version
|
||||
// is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
|
||||
//
|
||||
// By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
|
||||
// earlier as an `Err` rather than as a panic.
|
||||
version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
|
||||
}
|
||||
|
||||
fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
|
||||
if data.len() == 6 {
|
||||
Some(SemanticVersion::new(
|
||||
u16::from_be_bytes([data[0], data[1]]) as _,
|
||||
u16::from_be_bytes([data[2], data[3]]) as _,
|
||||
u16::from_be_bytes([data[4], data[5]]) as _,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::wasm_host::parse_wasm_extension_version;
|
||||
use crate::ExtensionManifest;
|
||||
use crate::{extension_manifest::ExtensionLibraryKind, GrammarManifestEntry};
|
||||
use crate::{
|
||||
parse_wasm_extension_version, ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
|
||||
@@ -16,7 +16,7 @@ path = "src/main.rs"
|
||||
anyhow.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
env_logger.workspace = true
|
||||
extension = { workspace = true, features = ["no-webrtc"] }
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -15,7 +15,6 @@ use extension::{
|
||||
};
|
||||
use language::LanguageConfig;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use theme::ThemeRegistry;
|
||||
use tree_sitter::{Language, Query, WasmStore};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -267,7 +266,7 @@ async fn test_themes(
|
||||
) -> Result<()> {
|
||||
for relative_theme_path in &manifest.themes {
|
||||
let theme_path = extension_path.join(relative_theme_path);
|
||||
let theme_family = ThemeRegistry::read_user_theme(&theme_path, fs.clone()).await?;
|
||||
let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?;
|
||||
log::info!("loaded theme family {}", theme_family.name);
|
||||
}
|
||||
|
||||
|
||||
62
crates/extension_host/Cargo.toml
Normal file
62
crates/extension_host/Cargo.toml
Normal file
@@ -0,0 +1,62 @@
|
||||
[package]
|
||||
name = "extension_host"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/extension_host.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
wasmtime.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
reqwest_client.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
1
crates/extension_host/LICENSE-GPL
Symbolic link
1
crates/extension_host/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,7 +1,5 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_indexed_docs_provider;
|
||||
mod extension_lsp_adapter;
|
||||
mod extension_manifest;
|
||||
mod extension_settings;
|
||||
mod extension_slash_command;
|
||||
mod wasm_host;
|
||||
@@ -10,7 +8,6 @@ mod wasm_host;
|
||||
mod extension_store_test;
|
||||
|
||||
use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
|
||||
use crate::extension_manifest::SchemaVersion;
|
||||
use crate::extension_slash_command::ExtensionSlashCommand;
|
||||
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
@@ -19,7 +16,8 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
|
||||
use collections::{btree_map, BTreeMap, HashSet};
|
||||
use extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::SchemaVersion;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
channel::{
|
||||
@@ -62,7 +60,7 @@ use wasm_host::{
|
||||
WasmExtension, WasmHost,
|
||||
};
|
||||
|
||||
pub use extension_manifest::{
|
||||
pub use extension::{
|
||||
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, OldExtensionManifest,
|
||||
};
|
||||
pub use extension_settings::ExtensionSettings;
|
||||
@@ -1358,7 +1356,7 @@ impl ExtensionStore {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(theme_family) = ThemeRegistry::read_user_theme(&theme_path, fs.clone())
|
||||
let Some(theme_family) = theme::read_user_theme(&theme_path, fs.clone())
|
||||
.await
|
||||
.log_err()
|
||||
else {
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::extension_manifest::SchemaVersion;
|
||||
use crate::extension_settings::ExtensionSettings;
|
||||
use crate::{
|
||||
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
|
||||
@@ -8,6 +7,7 @@ use crate::{
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use extension::SchemaVersion;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{Context, SemanticVersion, TestAppContext};
|
||||
@@ -296,7 +296,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
["ERB", "Plain Text", "Ruby"]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
@@ -377,7 +377,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
assert_eq!(index.themes, expected_index.themes);
|
||||
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
@@ -424,7 +424,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
["embedded_template".into(), "ruby".into()]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
theme_registry.list_names(),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
@@ -1,7 +1,7 @@
|
||||
pub(crate) mod wit;
|
||||
|
||||
use crate::ExtensionManifest;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use fs::{normalize_path, Fs};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use futures::{
|
||||
@@ -112,7 +112,8 @@ impl WasmHost {
|
||||
) -> Task<Result<WasmExtension>> {
|
||||
let this = self.clone();
|
||||
executor.clone().spawn(async move {
|
||||
let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
|
||||
let zed_api_version =
|
||||
extension::parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
|
||||
|
||||
let component = Component::from_binary(&this.engine, &wasm_bytes)
|
||||
.context("failed to compile wasm component")?;
|
||||
@@ -197,49 +198,6 @@ impl WasmHost {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_wasm_extension_version(
|
||||
extension_id: &str,
|
||||
wasm_bytes: &[u8],
|
||||
) -> Result<SemanticVersion> {
|
||||
let mut version = None;
|
||||
|
||||
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
|
||||
if let wasmparser::Payload::CustomSection(s) =
|
||||
part.context("error parsing wasm extension")?
|
||||
{
|
||||
if s.name() == "zed:api-version" {
|
||||
version = parse_wasm_extension_version_custom_section(s.data());
|
||||
if version.is_none() {
|
||||
bail!(
|
||||
"extension {} has invalid zed:api-version section: {:?}",
|
||||
extension_id,
|
||||
s.data()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reason we wait until we're done parsing all of the Wasm bytes to return the version
|
||||
// is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
|
||||
//
|
||||
// By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
|
||||
// earlier as an `Err` rather than as a panic.
|
||||
version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
|
||||
}
|
||||
|
||||
fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
|
||||
if data.len() == 6 {
|
||||
Some(SemanticVersion::new(
|
||||
u16::from_be_bytes([data[0], data[1]]) as _,
|
||||
u16::from_be_bytes([data[2], data[3]]) as _,
|
||||
u16::from_be_bytes([data[4], data[5]]) as _,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmExtension {
|
||||
pub async fn call<T, Fn>(&self, f: Fn) -> T
|
||||
where
|
||||
@@ -20,7 +20,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::{Arc, OnceLock};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use extension_host::ExtensionStore;
|
||||
use gpui::{Model, VisualContext};
|
||||
use language::Buffer;
|
||||
use ui::{SharedString, ViewContext};
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::ExtensionMetadata;
|
||||
use extension::{ExtensionSettings, ExtensionStore};
|
||||
use extension_host::{ExtensionSettings, ExtensionStore};
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -167,7 +167,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let extension_version = &self.extension_versions[candidate_id];
|
||||
|
||||
if !extension::is_version_compatible(ReleaseChannel::global(cx), extension_version) {
|
||||
if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
let extension_version = &self.extension_versions[version_match.candidate_id];
|
||||
|
||||
let is_version_compatible =
|
||||
extension::is_version_compatible(ReleaseChannel::global(cx), extension_version);
|
||||
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version);
|
||||
let disabled = !is_version_compatible;
|
||||
|
||||
Some(
|
||||
|
||||
@@ -11,7 +11,7 @@ use client::telemetry::Telemetry;
|
||||
use client::ExtensionMetadata;
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement,
|
||||
@@ -203,8 +203,8 @@ impl ExtensionsPage {
|
||||
let subscriptions = [
|
||||
cx.observe(&store, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&store, move |this, _, event, cx| match event {
|
||||
extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
|
||||
extension::Event::ExtensionInstalled(extension_id) => {
|
||||
extension_host::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx),
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => {
|
||||
this.on_extension_installed(workspace_handle.clone(), extension_id, cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -691,7 +691,8 @@ impl ExtensionsPage {
|
||||
has_dev_extension: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> (Button, Option<Button>) {
|
||||
let is_compatible = extension::is_version_compatible(ReleaseChannel::global(cx), extension);
|
||||
let is_compatible =
|
||||
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension);
|
||||
|
||||
if has_dev_extension {
|
||||
// If we have a dev extension for the given extension, just treat it as uninstalled.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{bail, Context};
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
use std::{
|
||||
fmt,
|
||||
fmt::{self, Display, Formatter},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
@@ -279,6 +279,19 @@ impl Hash for Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Hsla {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"hsla({:.2}, {:.2}%, {:.2}%, {:.2})",
|
||||
self.h * 360.,
|
||||
self.s * 100.,
|
||||
self.l * 100.,
|
||||
self.a
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an [`Hsla`] object from plain values
|
||||
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
|
||||
Hsla {
|
||||
|
||||
@@ -915,6 +915,12 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Track the scroll state of this element with the given handle.
|
||||
fn anchor_scroll(mut self, scroll_anchor: Option<ScrollAnchor>) -> Self {
|
||||
self.interactivity().scroll_anchor = scroll_anchor;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element is active.
|
||||
fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
@@ -1156,6 +1162,9 @@ impl Element for Div {
|
||||
) -> Option<Hitbox> {
|
||||
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||
let mut child_max = Point::default();
|
||||
if let Some(handle) = self.interactivity.scroll_anchor.as_ref() {
|
||||
*handle.last_origin.borrow_mut() = bounds.origin - cx.element_offset();
|
||||
}
|
||||
let content_size = if request_layout.child_layout_ids.is_empty() {
|
||||
bounds.size
|
||||
} else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() {
|
||||
@@ -1245,6 +1254,7 @@ pub struct Interactivity {
|
||||
pub(crate) focusable: bool,
|
||||
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
||||
pub(crate) tracked_scroll_handle: Option<ScrollHandle>,
|
||||
pub(crate) scroll_anchor: Option<ScrollAnchor>,
|
||||
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||
pub(crate) group: Option<SharedString>,
|
||||
/// The base style of the element, before any modifications are applied
|
||||
@@ -2091,7 +2101,6 @@ impl Interactivity {
|
||||
}
|
||||
scroll_offset.y += delta_y;
|
||||
scroll_offset.x += delta_x;
|
||||
|
||||
cx.stop_propagation();
|
||||
if *scroll_offset != old_scroll_offset {
|
||||
cx.refresh();
|
||||
@@ -2454,6 +2463,34 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an element that can be scrolled *to* in its parent element.
|
||||
///
|
||||
/// Contrary to [ScrollHandle::scroll_to_item], an anchored element does not have to be an immediate child of the parent.
|
||||
#[derive(Clone)]
|
||||
pub struct ScrollAnchor {
|
||||
handle: ScrollHandle,
|
||||
last_origin: Rc<RefCell<Point<Pixels>>>,
|
||||
}
|
||||
|
||||
impl ScrollAnchor {
|
||||
/// Creates a [ScrollAnchor] associated with a given [ScrollHandle].
|
||||
pub fn for_handle(handle: ScrollHandle) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
last_origin: Default::default(),
|
||||
}
|
||||
}
|
||||
/// Request scroll to this item on the next frame.
|
||||
pub fn scroll_to(&self, cx: &mut WindowContext<'_>) {
|
||||
let this = self.clone();
|
||||
|
||||
cx.on_next_frame(move |_| {
|
||||
let viewport_bounds = this.handle.bounds();
|
||||
let self_bounds = *this.last_origin.borrow();
|
||||
this.handle.set_offset(viewport_bounds.origin - self_bounds);
|
||||
});
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug)]
|
||||
struct ScrollHandleState {
|
||||
offset: Rc<RefCell<Point<Pixels>>>,
|
||||
|
||||
@@ -46,6 +46,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod logging;
|
||||
mod model;
|
||||
pub mod provider;
|
||||
mod rate_limiter;
|
||||
@@ -59,6 +60,7 @@ pub enum LanguageModelCompletionEvent {
|
||||
Stop(StopReason),
|
||||
Text(String),
|
||||
ToolUse(LanguageModelToolUse),
|
||||
StartMessage { message_id: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
@@ -76,6 +78,20 @@ pub struct LanguageModelToolUse {
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
pub struct LanguageModelTextStream {
|
||||
pub message_id: Option<String>,
|
||||
pub stream: BoxStream<'static, Result<String>>,
|
||||
}
|
||||
|
||||
impl Default for LanguageModelTextStream {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
message_id: None,
|
||||
stream: Box::pin(futures::stream::empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModel: Send + Sync {
|
||||
fn id(&self) -> LanguageModelId;
|
||||
fn name(&self) -> LanguageModelName;
|
||||
@@ -87,6 +103,10 @@ pub trait LanguageModel: Send + Sync {
|
||||
fn provider_name(&self) -> LanguageModelProviderName;
|
||||
fn telemetry_id(&self) -> String;
|
||||
|
||||
fn api_key(&self, _cx: &AppContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the availability of this language model.
|
||||
fn availability(&self) -> LanguageModelAvailability {
|
||||
LanguageModelAvailability::Public
|
||||
@@ -113,21 +133,39 @@ pub trait LanguageModel: Send + Sync {
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncAppContext,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
|
||||
let events = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
Ok(events
|
||||
.await?
|
||||
.filter_map(|result| async move {
|
||||
let mut events = events.await?;
|
||||
let mut message_id = None;
|
||||
let mut first_item_text = None;
|
||||
|
||||
if let Some(first_event) = events.next().await {
|
||||
match first_event {
|
||||
Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => {
|
||||
message_id = Some(id.clone());
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => {
|
||||
first_item_text = Some(text);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let stream = futures::stream::iter(first_item_text.map(Ok))
|
||||
.chain(events.filter_map(|result| async move {
|
||||
match result {
|
||||
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
|
||||
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
}
|
||||
})
|
||||
.boxed())
|
||||
}))
|
||||
.boxed();
|
||||
|
||||
Ok(LanguageModelTextStream { message_id, stream })
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
90
crates/language_model/src/logging.rs
Normal file
90
crates/language_model/src/logging.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use anthropic::{AnthropicError, ANTHROPIC_API_URL};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::BackgroundExecutor;
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::provider::anthropic::PROVIDER_ID as ANTHROPIC_PROVIDER_ID;
|
||||
|
||||
pub fn report_assistant_event(
|
||||
event: AssistantEvent,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
model_api_key: Option<String>,
|
||||
executor: &BackgroundExecutor,
|
||||
) {
|
||||
if let Some(telemetry) = telemetry.as_ref() {
|
||||
telemetry.report_assistant_event(event.clone());
|
||||
if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID {
|
||||
executor
|
||||
.spawn(async move {
|
||||
report_anthropic_event(event, client, model_api_key)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_anthropic_event(
|
||||
event: AssistantEvent,
|
||||
client: Arc<dyn HttpClient>,
|
||||
model_api_key: Option<String>,
|
||||
) -> Result<(), AnthropicError> {
|
||||
let api_key = match model_api_key {
|
||||
Some(key) => key,
|
||||
None => {
|
||||
return Err(AnthropicError::Other(anyhow!(
|
||||
"Anthropic API key is not set"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let uri = format!("{ANTHROPIC_API_URL}/v1/log/zed");
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("Content-Type", "application/json");
|
||||
let serialized_event: serde_json::Value = serde_json::json!({
|
||||
"completion_type": match event.kind {
|
||||
AssistantKind::Inline => "natural_language_completion_in_editor",
|
||||
AssistantKind::InlineTerminal => "natural_language_completion_in_terminal",
|
||||
AssistantKind::Panel => "conversation_message",
|
||||
},
|
||||
"event": match event.phase {
|
||||
AssistantPhase::Response => "response",
|
||||
AssistantPhase::Invoked => "invoke",
|
||||
AssistantPhase::Accepted => "accept",
|
||||
AssistantPhase::Rejected => "reject",
|
||||
},
|
||||
"metadata": {
|
||||
"language_name": event.language_name,
|
||||
"message_id": event.message_id,
|
||||
"platform": env::consts::OS,
|
||||
}
|
||||
});
|
||||
|
||||
let request = request_builder
|
||||
.body(AsyncBody::from(serialized_event.to_string()))
|
||||
.context("failed to construct request body")?;
|
||||
|
||||
let response = client
|
||||
.send(request)
|
||||
.await
|
||||
.context("failed to send request to Anthropic")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(AnthropicError::Other(anyhow!(
|
||||
"Failed to log: {}",
|
||||
response.status(),
|
||||
)));
|
||||
}
|
||||
@@ -26,7 +26,7 @@ use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Icon, IconName, Tooltip};
|
||||
use util::{maybe, ResultExt};
|
||||
|
||||
const PROVIDER_ID: &str = "anthropic";
|
||||
pub const PROVIDER_ID: &str = "anthropic";
|
||||
const PROVIDER_NAME: &str = "Anthropic";
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
@@ -356,6 +356,10 @@ impl LanguageModel for AnthropicModel {
|
||||
format!("anthropic/{}", self.model.id())
|
||||
}
|
||||
|
||||
fn api_key(&self, cx: &AppContext) -> Option<String> {
|
||||
self.state.read(cx).api_key.clone()
|
||||
}
|
||||
|
||||
fn max_token_count(&self) -> usize {
|
||||
self.model.max_token_count()
|
||||
}
|
||||
@@ -520,6 +524,14 @@ pub fn map_to_language_model_completion_events(
|
||||
));
|
||||
}
|
||||
}
|
||||
Event::MessageStart { message } => {
|
||||
return Some((
|
||||
Some(Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
})),
|
||||
state,
|
||||
))
|
||||
}
|
||||
Event::MessageDelta { delta, .. } => {
|
||||
if let Some(stop_reason) = delta.stop_reason.as_deref() {
|
||||
let stop_reason = match stop_reason {
|
||||
|
||||
@@ -38,7 +38,6 @@ async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
||||
@@ -3,7 +3,6 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
|
||||
@@ -77,13 +76,11 @@ impl JsonLspAdapter {
|
||||
|
||||
fn get_workspace_config(language_names: Vec<String>, cx: &mut AppContext) -> Value {
|
||||
let action_names = cx.all_action_names();
|
||||
let staff_mode = cx.is_staff();
|
||||
|
||||
let font_names = &cx.text_system().all_font_names();
|
||||
let settings_schema = cx.global::<SettingsStore>().json_schema(
|
||||
&SettingsJsonSchemaParams {
|
||||
language_names: &language_names,
|
||||
staff_mode,
|
||||
font_names,
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::ops::Range;
|
||||
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
let mut options = Options::all();
|
||||
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
|
||||
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut within_link = false;
|
||||
|
||||
@@ -3083,6 +3083,58 @@ impl MultiBufferSnapshot {
|
||||
summaries
|
||||
}
|
||||
|
||||
pub fn dimensions_from_points<'a, D>(
|
||||
&'a self,
|
||||
points: impl 'a + IntoIterator<Item = Point>,
|
||||
) -> impl 'a + Iterator<Item = D>
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
let mut cursor = self.excerpts.cursor::<TextSummary>(&());
|
||||
let mut memoized_source_start: Option<Point> = None;
|
||||
let mut points = points.into_iter();
|
||||
std::iter::from_fn(move || {
|
||||
let point = points.next()?;
|
||||
|
||||
// Clear the memoized source start if the point is in a different excerpt than previous.
|
||||
if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) {
|
||||
memoized_source_start = None;
|
||||
}
|
||||
|
||||
// Now determine where the excerpt containing the point starts in its source buffer.
|
||||
// We'll use this value to calculate overshoot next.
|
||||
let source_start = if let Some(source_start) = memoized_source_start {
|
||||
source_start
|
||||
} else {
|
||||
cursor.seek_forward(&point, Bias::Right, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
let source_start = excerpt.range.context.start.to_point(&excerpt.buffer);
|
||||
memoized_source_start = Some(source_start);
|
||||
source_start
|
||||
} else {
|
||||
return Some(D::from_text_summary(cursor.start()));
|
||||
}
|
||||
};
|
||||
|
||||
// First, assume the output dimension is at least the start of the excerpt containing the point
|
||||
let mut output = D::from_text_summary(cursor.start());
|
||||
|
||||
// If the point lands within its excerpt, calculate and add the overshoot in dimension D.
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
let overshoot = point - cursor.start().lines;
|
||||
if !overshoot.is_zero() {
|
||||
let end_in_excerpt = source_start + overshoot;
|
||||
output.add_assign(
|
||||
&excerpt
|
||||
.buffer
|
||||
.text_summary_for_range::<D, _>(source_start..end_in_excerpt),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(output)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)>
|
||||
where
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
@@ -4716,6 +4768,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point {
|
||||
fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(self, &cursor_location.lines)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(&Some(self), cursor_location)
|
||||
|
||||
@@ -1440,26 +1440,26 @@ impl OutlinePanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal_entry_for_selection(
|
||||
&mut self,
|
||||
editor: &View<Editor>,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) {
|
||||
fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<'_, Self>) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
|
||||
return;
|
||||
}
|
||||
let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else {
|
||||
self.selected_entry = SelectedEntry::None;
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
|
||||
let project = self.project.clone();
|
||||
self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move {
|
||||
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
|
||||
let entry_with_selection = outline_panel.update(&mut cx, |outline_panel, cx| {
|
||||
outline_panel.location_for_editor_selection(&editor, cx)
|
||||
})?;
|
||||
let Some(entry_with_selection) = entry_with_selection else {
|
||||
outline_panel.update(&mut cx, |outline_panel, cx| {
|
||||
outline_panel.selected_entry = SelectedEntry::None;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
};
|
||||
let related_buffer_entry = match &entry_with_selection {
|
||||
PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
|
||||
project.update(&mut cx, |project, cx| {
|
||||
@@ -2436,7 +2436,7 @@ impl OutlinePanel {
|
||||
}
|
||||
|
||||
fn location_for_editor_selection(
|
||||
&mut self,
|
||||
&self,
|
||||
editor: &View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<PanelEntry> {
|
||||
@@ -2500,7 +2500,7 @@ impl OutlinePanel {
|
||||
}
|
||||
|
||||
fn outline_location(
|
||||
&mut self,
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
excerpt_id: ExcerptId,
|
||||
multi_buffer_snapshot: editor::MultiBufferSnapshot,
|
||||
@@ -4261,7 +4261,7 @@ impl Render for OutlinePanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.track_focus(&self.focus_handle)
|
||||
.when_some(search_query, |outline_panel, search_state| {
|
||||
outline_panel.child(
|
||||
v_flex()
|
||||
@@ -4321,7 +4321,7 @@ fn subscribe_for_editor_events(
|
||||
editor,
|
||||
move |outline_panel, editor, e: &EditorEvent, cx| match e {
|
||||
EditorEvent::SelectionsChanged { local: true } => {
|
||||
outline_panel.reveal_entry_for_selection(&editor, cx);
|
||||
outline_panel.reveal_entry_for_selection(editor, cx);
|
||||
cx.notify();
|
||||
}
|
||||
EditorEvent::ExcerptsAdded { excerpts, .. } => {
|
||||
@@ -4479,7 +4479,13 @@ mod tests {
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, _| {
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
// Project search re-adds items to the buffer, removing the caret from it.
|
||||
// Select the first entry and move 4 elements down.
|
||||
for _ in 0..6 {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&outline_panel.cached_entries,
|
||||
@@ -4795,7 +4801,7 @@ mod tests {
|
||||
r#"/
|
||||
public/lottie/
|
||||
syntax-tree.json
|
||||
search: { "something": "static" } <==== selected
|
||||
search: { "something": "static" }
|
||||
src/
|
||||
app/(site)/
|
||||
(about)/jobs/[slug]/
|
||||
@@ -4811,8 +4817,11 @@ mod tests {
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
// After the search is done, we have updated the outline panel contents and caret is not in any excerot, so there are no selections.
|
||||
// Move to 5th element in the list (0th action will selection the first element)
|
||||
for _ in 0..6 {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
}
|
||||
outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -5282,6 +5282,10 @@ impl LspStore {
|
||||
self.last_formatting_failure.as_deref()
|
||||
}
|
||||
|
||||
pub fn reset_last_formatting_failure(&mut self) {
|
||||
self.last_formatting_failure = None;
|
||||
}
|
||||
|
||||
pub fn environment_for_buffer(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -2697,6 +2697,11 @@ impl Project {
|
||||
self.lsp_store.read(cx).last_formatting_failure()
|
||||
}
|
||||
|
||||
pub fn reset_last_formatting_failure(&self, cx: &mut AppContext) {
|
||||
self.lsp_store
|
||||
.update(cx, |store, _| store.reset_last_formatting_failure());
|
||||
}
|
||||
|
||||
pub fn update_diagnostics(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
|
||||
@@ -257,7 +257,6 @@ message Envelope {
|
||||
FindSearchCandidatesResponse find_search_candidates_response = 244;
|
||||
|
||||
CloseBuffer close_buffer = 245;
|
||||
UpdateUserSettings update_user_settings = 246;
|
||||
|
||||
ShutdownRemoteServer shutdown_remote_server = 257;
|
||||
|
||||
@@ -309,6 +308,7 @@ message Envelope {
|
||||
reserved 205 to 206;
|
||||
reserved 221;
|
||||
reserved 224 to 229;
|
||||
reserved 246;
|
||||
reserved 247 to 254;
|
||||
reserved 255 to 256;
|
||||
}
|
||||
@@ -2361,17 +2361,6 @@ message AddWorktreeResponse {
|
||||
string canonicalized_path = 2;
|
||||
}
|
||||
|
||||
message UpdateUserSettings {
|
||||
uint64 project_id = 1;
|
||||
string content = 2;
|
||||
optional Kind kind = 3;
|
||||
|
||||
enum Kind {
|
||||
Settings = 0;
|
||||
Tasks = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GetPathMetadata {
|
||||
uint64 project_id = 1;
|
||||
string path = 2;
|
||||
|
||||
@@ -342,7 +342,6 @@ messages!(
|
||||
(FindSearchCandidates, Background),
|
||||
(FindSearchCandidatesResponse, Background),
|
||||
(CloseBuffer, Foreground),
|
||||
(UpdateUserSettings, Foreground),
|
||||
(ShutdownRemoteServer, Foreground),
|
||||
(RemoveWorktree, Foreground),
|
||||
(LanguageServerLog, Foreground),
|
||||
@@ -559,7 +558,6 @@ entity_messages!(
|
||||
UpdateContext,
|
||||
SynchronizeContexts,
|
||||
LspExtSwitchSourceHeader,
|
||||
UpdateUserSettings,
|
||||
LanguageServerLog,
|
||||
Toast,
|
||||
HideToast,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,8 @@ use gpui::{AppContext, Model};
|
||||
|
||||
use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use remote::ssh_session::{ServerBinary, ServerVersion};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::ssh_session::ConnectionIdentifier;
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -64,7 +64,7 @@ impl SshSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
|
||||
pub struct SshConnection {
|
||||
pub host: SharedString,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -101,7 +101,7 @@ impl From<SshConnection> for SshConnectionOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct SshProject {
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
@@ -441,23 +441,66 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn get_server_binary(
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_over_ssh: bool,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let this = self.clone();
|
||||
) -> Task<anyhow::Result<PathBuf>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
tx.send(
|
||||
this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx)
|
||||
.await,
|
||||
let binary_path = AutoUpdater::download_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
version,
|
||||
&mut cx,
|
||||
)
|
||||
.ok();
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
|
||||
version
|
||||
.map(|v| format!("{}", v))
|
||||
.unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
Ok(binary_path)
|
||||
})
|
||||
.detach();
|
||||
rx
|
||||
}
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<(String, String)>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
version,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
|
||||
version.map(|v| format!("{}", v)).unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((release.url, request_body))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn remote_server_binary_path(
|
||||
@@ -485,208 +528,6 @@ impl SshClientDelegate {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
async fn get_server_binary_impl(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_via_ssh: bool,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(ServerBinary, ServerVersion)> {
|
||||
let (version, release_channel) = cx.update(|cx| {
|
||||
let version = AppVersion::global(cx);
|
||||
let channel = ReleaseChannel::global(cx);
|
||||
|
||||
(version, channel)
|
||||
})?;
|
||||
|
||||
// In dev mode, build the remote server binary from source
|
||||
#[cfg(debug_assertions)]
|
||||
if release_channel == ReleaseChannel::Dev {
|
||||
let result = self.build_local(cx, platform, version).await?;
|
||||
// Fall through to a remote binary if we're not able to compile a local binary
|
||||
if let Some((path, version)) = result {
|
||||
return Ok((
|
||||
ServerBinary::LocalBinary(path),
|
||||
ServerVersion::Semantic(version),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// For nightly channel, always get latest
|
||||
let current_version = if release_channel == ReleaseChannel::Nightly {
|
||||
None
|
||||
} else {
|
||||
Some(version)
|
||||
};
|
||||
|
||||
self.update_status(
|
||||
Some(&format!("Checking remote server release {}", version)),
|
||||
cx,
|
||||
);
|
||||
|
||||
if upload_binary_via_ssh {
|
||||
let binary_path = AutoUpdater::download_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
current_version,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
|
||||
version,
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
ServerBinary::LocalBinary(binary_path),
|
||||
ServerVersion::Semantic(version),
|
||||
))
|
||||
} else {
|
||||
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
current_version,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
|
||||
version,
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let version = release
|
||||
.version
|
||||
.parse::<SemanticVersion>()
|
||||
.map(ServerVersion::Semantic)
|
||||
.unwrap_or_else(|_| ServerVersion::Commit(release.version));
|
||||
Ok((
|
||||
ServerBinary::ReleaseUrl {
|
||||
url: release.url,
|
||||
body: request_body,
|
||||
},
|
||||
version,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_local(
|
||||
&self,
|
||||
cx: &mut AsyncAppContext,
|
||||
platform: SshPlatform,
|
||||
version: gpui::SemanticVersion,
|
||||
) -> Result<Option<(PathBuf, gpui::SemanticVersion)>> {
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("Failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
||||
self.update_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
self.update_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
"target/remote_server/debug/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
||||
return Ok(Some((path, version)));
|
||||
} else if let Some(triple) = platform.triple() {
|
||||
smol::fs::create_dir_all("target/remote_server").await?;
|
||||
|
||||
self.update_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
self.update_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
"--mount type=bind,src=./target,dst=/app/target",
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.update_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join(format!(
|
||||
"target/remote_server/{}/debug/remote_server.gz",
|
||||
triple
|
||||
));
|
||||
|
||||
return Ok(Some((path, version)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {
|
||||
@@ -694,7 +535,7 @@ pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {
|
||||
}
|
||||
|
||||
pub fn connect_over_ssh(
|
||||
unique_identifier: String,
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: SshConnectionOptions,
|
||||
ui: View<SshPrompt>,
|
||||
cx: &mut WindowContext,
|
||||
|
||||
@@ -24,6 +24,7 @@ collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prost.workspace = true
|
||||
@@ -35,6 +36,7 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -20,7 +20,9 @@ use gpui::{
|
||||
AppContext, AsyncAppContext, BorrowAppContext, Context, EventEmitter, Global, Model,
|
||||
ModelContext, SemanticVersion, Task, WeakModel,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use rpc::{
|
||||
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
|
||||
AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet,
|
||||
@@ -33,8 +35,7 @@ use smol::{
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::VecDeque,
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fmt, iter,
|
||||
ops::ControlFlow,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
@@ -69,6 +70,18 @@ pub struct SshConnectionOptions {
|
||||
pub upload_binary_over_ssh: bool,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! shell_script {
|
||||
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
|
||||
format!(
|
||||
$fmt,
|
||||
$(
|
||||
$name = shlex::try_quote($arg).unwrap()
|
||||
),+
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
impl SshConnectionOptions {
|
||||
pub fn parse_command_line(input: &str) -> Result<Self> {
|
||||
let input = input.trim_start_matches("ssh ");
|
||||
@@ -189,17 +202,6 @@ impl SshConnectionOptions {
|
||||
host
|
||||
}
|
||||
}
|
||||
|
||||
// Uniquely identifies dev server projects on a remote host. Needs to be
|
||||
// stable for the same dev server project.
|
||||
pub fn remote_server_identifier(&self) -> String {
|
||||
let mut identifier = format!("dev-server-{:?}", self.host);
|
||||
if let Some(username) = self.username.as_ref() {
|
||||
identifier.push('-');
|
||||
identifier.push_str(&username);
|
||||
}
|
||||
identifier
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -227,10 +229,19 @@ pub enum ServerBinary {
|
||||
ReleaseUrl { url: String, body: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ServerVersion {
|
||||
Semantic(SemanticVersion),
|
||||
Commit(String),
|
||||
}
|
||||
impl ServerVersion {
|
||||
pub fn semantic_version(&self) -> Option<SemanticVersion> {
|
||||
match self {
|
||||
Self::Semantic(version) => Some(*version),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -252,24 +263,45 @@ pub trait SshClientDelegate: Send + Sync {
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf>;
|
||||
fn get_server_binary(
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_over_ssh: bool,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>>;
|
||||
) -> Task<Result<(String, String)>>;
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<PathBuf>>;
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
|
||||
}
|
||||
|
||||
impl SshSocket {
|
||||
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||
// :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
|
||||
// e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
|
||||
// and passes -l as an argument to sh, not to ls.
|
||||
// You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'"
|
||||
fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command {
|
||||
let mut command = process::Command::new("ssh");
|
||||
let to_run = iter::once(&program)
|
||||
.chain(args.iter())
|
||||
.map(|token| shlex::try_quote(token).unwrap())
|
||||
.join(" ");
|
||||
self.ssh_options(&mut command)
|
||||
.arg(self.connection_options.ssh_url())
|
||||
.arg(program);
|
||||
.arg(to_run);
|
||||
command
|
||||
}
|
||||
|
||||
fn shell_script(&self, script: impl AsRef<str>) -> process::Command {
|
||||
return self.ssh_command("sh", &["-c", script.as_ref()]);
|
||||
}
|
||||
|
||||
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
@@ -290,7 +322,7 @@ impl SshSocket {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
async fn run_cmd(mut command: process::Command) -> Result<String> {
|
||||
let output = command.output().await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
@@ -477,14 +509,43 @@ pub enum SshRemoteEvent {
|
||||
|
||||
impl EventEmitter<SshRemoteEvent> for SshRemoteClient {}
|
||||
|
||||
// Identifies the socket on the remote server so that reconnects
|
||||
// can re-join the same project.
|
||||
pub enum ConnectionIdentifier {
|
||||
Setup,
|
||||
Workspace(i64),
|
||||
}
|
||||
|
||||
impl ConnectionIdentifier {
|
||||
// This string gets used in a socket name, and so must be relatively short.
|
||||
// The total length of:
|
||||
// /home/{username}/.local/share/zed/server_state/{name}/stdout.sock
|
||||
// Must be less than about 100 characters
|
||||
// https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars
|
||||
// So our strings should be at most 20 characters or so.
|
||||
fn to_string(&self, cx: &AppContext) -> String {
|
||||
let identifier_prefix = match ReleaseChannel::global(cx) {
|
||||
ReleaseChannel::Stable => "".to_string(),
|
||||
release_channel => format!("{}-", release_channel.dev_name()),
|
||||
};
|
||||
match self {
|
||||
Self::Setup => format!("{identifier_prefix}setup"),
|
||||
Self::Workspace(workspace_id) => {
|
||||
format!("{identifier_prefix}workspace-{workspace_id}",)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshRemoteClient {
|
||||
pub fn new(
|
||||
unique_identifier: String,
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: SshConnectionOptions,
|
||||
cancellation: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<Model<Self>>>> {
|
||||
let unique_identifier = unique_identifier.to_string(cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let success = Box::pin(async move {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
@@ -1053,7 +1114,15 @@ impl SshRemoteClient {
|
||||
) -> Model<Self> {
|
||||
let (_tx, rx) = oneshot::channel();
|
||||
client_cx
|
||||
.update(|cx| Self::new("fake".to_string(), opts, rx, Arc::new(fake::Delegate), cx))
|
||||
.update(|cx| {
|
||||
Self::new(
|
||||
ConnectionIdentifier::Setup,
|
||||
opts,
|
||||
rx,
|
||||
Arc::new(fake::Delegate),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
@@ -1217,7 +1286,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
}
|
||||
|
||||
let socket = self.socket.clone();
|
||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||
run_cmd(socket.ssh_command(&remote_binary_path.to_string_lossy(), &["version"])).await?;
|
||||
Ok(remote_binary_path)
|
||||
}
|
||||
|
||||
@@ -1234,22 +1303,33 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
) -> Task<Result<i32>> {
|
||||
delegate.set_status(Some("Starting proxy"), cx);
|
||||
|
||||
let mut start_proxy_command = format!(
|
||||
"RUST_LOG={} {} {:?} proxy --identifier {}",
|
||||
std::env::var("RUST_LOG").unwrap_or_default(),
|
||||
std::env::var("RUST_BACKTRACE")
|
||||
.map(|b| { format!("RUST_BACKTRACE={}", b) })
|
||||
.unwrap_or_default(),
|
||||
remote_binary_path,
|
||||
unique_identifier,
|
||||
let mut start_proxy_command = shell_script!(
|
||||
"exec {binary_path} proxy --identifier {identifier}",
|
||||
binary_path = &remote_binary_path.to_string_lossy(),
|
||||
identifier = &unique_identifier,
|
||||
);
|
||||
|
||||
if let Some(rust_log) = std::env::var("RUST_LOG").ok() {
|
||||
start_proxy_command = format!(
|
||||
"RUST_LOG={} {}",
|
||||
shlex::try_quote(&rust_log).unwrap(),
|
||||
start_proxy_command
|
||||
)
|
||||
}
|
||||
if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() {
|
||||
start_proxy_command = format!(
|
||||
"RUST_BACKTRACE={} {}",
|
||||
shlex::try_quote(&rust_backtrace).unwrap(),
|
||||
start_proxy_command
|
||||
)
|
||||
}
|
||||
if reconnect {
|
||||
start_proxy_command.push_str(" --reconnect");
|
||||
}
|
||||
|
||||
let ssh_proxy_process = match self
|
||||
.socket
|
||||
.ssh_command(start_proxy_command)
|
||||
.shell_script(start_proxy_command)
|
||||
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
@@ -1431,8 +1511,8 @@ impl SshRemoteConnection {
|
||||
socket_path,
|
||||
};
|
||||
|
||||
let os = run_cmd(socket.ssh_command("uname").arg("-s")).await?;
|
||||
let arch = run_cmd(socket.ssh_command("uname").arg("-m")).await?;
|
||||
let os = run_cmd(socket.ssh_command("uname", &["-s"])).await?;
|
||||
let arch = run_cmd(socket.ssh_command("uname", &["-m"])).await?;
|
||||
|
||||
let os = match os.trim() {
|
||||
"Darwin" => "macos",
|
||||
@@ -1630,14 +1710,9 @@ impl SshRemoteConnection {
|
||||
}
|
||||
|
||||
async fn get_ssh_source_port(&self) -> Result<String> {
|
||||
let output = run_cmd(
|
||||
self.socket
|
||||
.ssh_command("sh")
|
||||
.arg("-c")
|
||||
.arg(r#""echo $SSH_CLIENT | cut -d' ' -f2""#),
|
||||
)
|
||||
.await
|
||||
.context("failed to get source port from SSH_CLIENT on host")?;
|
||||
let output = run_cmd(self.socket.shell_script("echo $SSH_CLIENT | cut -d' ' -f2"))
|
||||
.await
|
||||
.context("failed to get source port from SSH_CLIENT on host")?;
|
||||
|
||||
Ok(output.trim().to_string())
|
||||
}
|
||||
@@ -1648,13 +1723,13 @@ impl SshRemoteConnection {
|
||||
.ok_or_else(|| anyhow!("Lock file path has no parent directory"))?;
|
||||
|
||||
let script = format!(
|
||||
r#"'mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists"'"#,
|
||||
r#"mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists""#,
|
||||
parent_dir = parent_dir.display(),
|
||||
lock_file = lock_file.display(),
|
||||
content = content,
|
||||
);
|
||||
|
||||
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
|
||||
let output = run_cmd(self.socket.shell_script(&script))
|
||||
.await
|
||||
.with_context(|| format!("failed to create a lock file at {:?}", lock_file))?;
|
||||
|
||||
@@ -1662,7 +1737,7 @@ impl SshRemoteConnection {
|
||||
}
|
||||
|
||||
fn generate_stale_check_script(lock_file: &Path, max_age: u64) -> String {
|
||||
format!(
|
||||
shell_script!(
|
||||
r#"
|
||||
if [ ! -f "{lock_file}" ]; then
|
||||
echo "lock file does not exist"
|
||||
@@ -1690,18 +1765,15 @@ impl SshRemoteConnection {
|
||||
else
|
||||
echo "recent"
|
||||
fi"#,
|
||||
lock_file = lock_file.display(),
|
||||
max_age = max_age
|
||||
lock_file = &lock_file.to_string_lossy(),
|
||||
max_age = &max_age.to_string()
|
||||
)
|
||||
}
|
||||
|
||||
async fn is_lock_stale(&self, lock_file: &Path, max_age: &Duration) -> Result<bool> {
|
||||
let script = format!(
|
||||
"'{}'",
|
||||
Self::generate_stale_check_script(lock_file, max_age.as_secs())
|
||||
);
|
||||
let script = Self::generate_stale_check_script(lock_file, max_age.as_secs());
|
||||
|
||||
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
|
||||
let output = run_cmd(self.socket.shell_script(script))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to check whether lock file {:?} is stale", lock_file)
|
||||
@@ -1714,9 +1786,12 @@ impl SshRemoteConnection {
|
||||
}
|
||||
|
||||
async fn remove_lock_file(&self, lock_file: &Path) -> Result<()> {
|
||||
run_cmd(self.socket.ssh_command("rm").arg("-f").arg(lock_file))
|
||||
.await
|
||||
.context("failed to remove lock file")?;
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("rm", &["-f", &lock_file.to_string_lossy()]),
|
||||
)
|
||||
.await
|
||||
.context("failed to remove lock file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1727,109 +1802,149 @@ impl SshRemoteConnection {
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() {
|
||||
if let Ok(installed_version) =
|
||||
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
|
||||
{
|
||||
log::info!("using cached server binary version {}", installed_version);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(not(debug_assertions)) {
|
||||
// When we're not in dev mode, we don't want to switch out the binary if it's
|
||||
// still open.
|
||||
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
|
||||
// to still replace the binary.
|
||||
if self.is_binary_in_use(dst_path).await? {
|
||||
log::info!("server binary is opened by another process. not updating");
|
||||
delegate.set_status(
|
||||
Some("Skipping update of remote development server, since it's still in use"),
|
||||
cx,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
|
||||
let (binary, new_server_version) = delegate
|
||||
.get_server_binary(platform, upload_binary_over_ssh, cx)
|
||||
.await??;
|
||||
|
||||
if cfg!(not(debug_assertions)) {
|
||||
let installed_version = if let Ok(version_output) =
|
||||
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
|
||||
{
|
||||
let current_version = match run_cmd(
|
||||
self.socket
|
||||
.ssh_command(&dst_path.to_string_lossy(), &["version"]),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(version_output) => {
|
||||
if let Ok(version) = version_output.trim().parse::<SemanticVersion>() {
|
||||
Some(ServerVersion::Semantic(version))
|
||||
} else {
|
||||
Some(ServerVersion::Commit(version_output.trim().to_string()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let (release_channel, wanted_version) = cx.update(|cx| {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let wanted_version = match release_channel {
|
||||
ReleaseChannel::Nightly => {
|
||||
AppCommitSha::try_global(cx).map(|sha| ServerVersion::Commit(sha.0))
|
||||
}
|
||||
ReleaseChannel::Dev => None,
|
||||
_ => Some(ServerVersion::Semantic(AppVersion::global(cx))),
|
||||
};
|
||||
(release_channel, wanted_version)
|
||||
})?;
|
||||
|
||||
if let Some(installed_version) = installed_version {
|
||||
use ServerVersion::*;
|
||||
match (installed_version, new_server_version) {
|
||||
(Semantic(installed), Semantic(new)) if installed == new => {
|
||||
log::info!("remote development server present and matching client version");
|
||||
return Ok(());
|
||||
}
|
||||
(Semantic(installed), Semantic(new)) if installed > new => {
|
||||
let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", installed, new);
|
||||
return Err(error);
|
||||
}
|
||||
(Commit(installed), Commit(new)) if installed == new => {
|
||||
log::info!(
|
||||
"remote development server present and matching client version {}",
|
||||
installed
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
(installed, _) => {
|
||||
log::info!(
|
||||
"remote development server has version: {}. updating...",
|
||||
installed
|
||||
);
|
||||
}
|
||||
match (¤t_version, &wanted_version) {
|
||||
(Some(current), Some(wanted)) if current == wanted => {
|
||||
log::info!("remote development server present and matching client version");
|
||||
return Ok(());
|
||||
}
|
||||
(Some(ServerVersion::Semantic(current)), Some(ServerVersion::Semantic(wanted)))
|
||||
if current > wanted =>
|
||||
{
|
||||
anyhow::bail!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", current, wanted);
|
||||
}
|
||||
_ => {
|
||||
log::info!("Installing remote development server");
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_binary_in_use(dst_path).await? {
|
||||
// When we're not in dev mode, we don't want to switch out the binary if it's
|
||||
// still open.
|
||||
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
|
||||
// to still replace the binary.
|
||||
if cfg!(not(debug_assertions)) {
|
||||
anyhow::bail!("The remote server version ({:?}) does not match the wanted version ({:?}), but is in use by another Zed client so cannot be upgraded.", ¤t_version, &wanted_version)
|
||||
} else {
|
||||
log::info!("Binary is currently in use, ignoring because this is a dev build")
|
||||
}
|
||||
}
|
||||
|
||||
if wanted_version.is_none() {
|
||||
if std::env::var("ZED_BUILD_REMOTE_SERVER").is_err() {
|
||||
if let Some(current_version) = current_version {
|
||||
log::warn!(
|
||||
"In development, using cached remote server binary version ({})",
|
||||
current_version
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"ZED_BUILD_REMOTE_SERVER is not set, but no remote server exists at ({:?})",
|
||||
dst_path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let src_path = self.build_local(platform, delegate, cx).await?;
|
||||
|
||||
return self
|
||||
.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
anyhow::bail!("Running development build in release mode, cannot cross compile (unset ZED_BUILD_REMOTE_SERVER)")
|
||||
}
|
||||
|
||||
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
|
||||
|
||||
if !upload_binary_over_ssh {
|
||||
let (url, body) = delegate
|
||||
.get_download_params(
|
||||
platform,
|
||||
release_channel,
|
||||
wanted_version.clone().and_then(|v| v.semantic_version()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match self
|
||||
.download_binary_on_server(&url, &body, dst_path, delegate, cx)
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to download binary on server, attempting to upload server: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match binary {
|
||||
ServerBinary::LocalBinary(src_path) => {
|
||||
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
ServerBinary::ReleaseUrl { url, body } => {
|
||||
self.download_binary_on_server(&url, &body, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
let src_path = delegate
|
||||
.download_server_binary_locally(
|
||||
platform,
|
||||
release_channel,
|
||||
wanted_version.and_then(|v| v.semantic_version()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn is_binary_in_use(&self, binary_path: &Path) -> Result<bool> {
|
||||
let script = format!(
|
||||
r#"'
|
||||
let script = shell_script!(
|
||||
r#"
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
if lsof "{}" >/dev/null 2>&1; then
|
||||
if lsof "{binary_path}" >/dev/null 2>&1; then
|
||||
echo "in_use"
|
||||
exit 0
|
||||
fi
|
||||
elif command -v fuser >/dev/null 2>&1; then
|
||||
if fuser "{}" >/dev/null 2>&1; then
|
||||
if fuser "{binary_path}" >/dev/null 2>&1; then
|
||||
echo "in_use"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "not_in_use"
|
||||
'"#,
|
||||
binary_path.display(),
|
||||
binary_path.display(),
|
||||
"#,
|
||||
binary_path = &binary_path.to_string_lossy(),
|
||||
);
|
||||
|
||||
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script))
|
||||
let output = run_cmd(self.socket.shell_script(script))
|
||||
.await
|
||||
.context("failed to check if binary is in use")?;
|
||||
|
||||
@@ -1848,31 +1963,32 @@ impl SshRemoteConnection {
|
||||
dst_path_gz.set_extension("gz");
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
delegate.set_status(Some("Downloading remote development server on host"), cx);
|
||||
|
||||
let script = format!(
|
||||
let script = shell_script!(
|
||||
r#"
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data='{}' '{}' -O '{}' && echo "wget"
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
curl -L -X GET -H "Content-Type: application/json" -d '{}' '{}' -o '{}' && echo "curl"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_path} && echo "curl"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_path} && echo "wget"
|
||||
else
|
||||
echo "Neither curl nor wget is available" >&2
|
||||
exit 1
|
||||
fi
|
||||
"#,
|
||||
body.replace("'", r#"\'"#),
|
||||
url,
|
||||
dst_path_gz.display(),
|
||||
body.replace("'", r#"\'"#),
|
||||
url,
|
||||
dst_path_gz.display(),
|
||||
body = body,
|
||||
url = url,
|
||||
dst_path = &dst_path_gz.to_string_lossy(),
|
||||
);
|
||||
|
||||
let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script))
|
||||
let output = run_cmd(self.socket.shell_script(script))
|
||||
.await
|
||||
.context("Failed to download server binary")?;
|
||||
|
||||
@@ -1895,7 +2011,11 @@ impl SshRemoteConnection {
|
||||
dst_path_gz.set_extension("gz");
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let src_stat = fs::metadata(&src_path).await?;
|
||||
@@ -1923,20 +2043,16 @@ impl SshRemoteConnection {
|
||||
delegate.set_status(Some("Extracting remote development server"), cx);
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("gunzip")
|
||||
.arg("--force")
|
||||
.arg(&dst_path_gz),
|
||||
.ssh_command("gunzip", &["-f", &dst_path_gz.to_string_lossy()]),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server_mode = 0o755;
|
||||
delegate.set_status(Some("Marking remote development server executable"), cx);
|
||||
run_cmd(
|
||||
self.socket
|
||||
.ssh_command("chmod")
|
||||
.arg(format!("{:o}", server_mode))
|
||||
.arg(dst_path),
|
||||
)
|
||||
run_cmd(self.socket.ssh_command(
|
||||
"chmod",
|
||||
&[&format!("{:o}", server_mode), &dst_path.to_string_lossy()],
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -1974,6 +2090,113 @@ impl SshRemoteConnection {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_local(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
delegate: &Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("Failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
||||
delegate.set_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
"target/remote_server/debug/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
||||
return Ok(path);
|
||||
}
|
||||
let Some(triple) = platform.triple() else {
|
||||
anyhow::bail!("can't cross compile for: {:?}", platform);
|
||||
};
|
||||
smol::fs::create_dir_all("target/remote_server").await?;
|
||||
|
||||
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
"--mount type=bind,src=./target,dst=/app/target",
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join(format!(
|
||||
"target/remote_server/{}/debug/remote_server.gz",
|
||||
triple
|
||||
));
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||
@@ -2295,12 +2518,12 @@ mod fake {
|
||||
},
|
||||
select_biased, FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{AsyncAppContext, Task, TestAppContext};
|
||||
use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext};
|
||||
use release_channel::ReleaseChannel;
|
||||
use rpc::proto::Envelope;
|
||||
|
||||
use super::{
|
||||
ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate,
|
||||
SshConnectionOptions, SshPlatform,
|
||||
ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
|
||||
};
|
||||
|
||||
pub(super) struct FakeRemoteConnection {
|
||||
@@ -2412,23 +2635,36 @@ mod fake {
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn remote_server_binary_path(
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: ReleaseChannel,
|
||||
_: Option<SemanticVersion>,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
) -> Task<Result<PathBuf>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn get_server_binary(
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: bool,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
|
||||
_platform: SshPlatform,
|
||||
_release_channel: ReleaseChannel,
|
||||
_version: Option<SemanticVersion>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<(String, String)>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {}
|
||||
|
||||
fn remote_server_binary_path(
|
||||
&self,
|
||||
_platform: SshPlatform,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::headless_project::HeadlessProject;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use fs::{FakeFs, Fs};
|
||||
use gpui::{Context, Model, TestAppContext};
|
||||
use gpui::{Context, Model, SemanticVersion, TestAppContext};
|
||||
use http_client::{BlockedHttpClient, FakeHttpClient};
|
||||
use language::{
|
||||
language_settings::{language_settings, AllLanguageSettings},
|
||||
@@ -1184,6 +1184,9 @@ pub async fn init_test(
|
||||
server_cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, Model<HeadlessProject>) {
|
||||
let server_fs = server_fs.clone();
|
||||
cx.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
});
|
||||
init_logger();
|
||||
|
||||
let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
#![allow(unused, dead_code)]
|
||||
use std::future::Future;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::proto::ViewId;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag};
|
||||
use futures::future::Shared;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions, list, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
ListScrollEvent, ListState, Model, Task,
|
||||
actions, list, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
ListScrollEvent, ListState, Model, Point, Task, View,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language::{Language, LanguageRegistry};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::item::ItemEvent;
|
||||
use workspace::item::{ItemEvent, TabContentParams};
|
||||
use workspace::searchable::SearchableItemHandle;
|
||||
use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation};
|
||||
use workspace::{ToolbarItemEvent, ToolbarItemView};
|
||||
|
||||
@@ -21,9 +25,6 @@ use super::{Cell, CellPosition, RenderableCell};
|
||||
use nbformat::v4::CellId;
|
||||
use nbformat::v4::Metadata as NotebookMetadata;
|
||||
|
||||
pub(crate) const DEFAULT_NOTEBOOK_FORMAT: i32 = 4;
|
||||
pub(crate) const DEFAULT_NOTEBOOK_FORMAT_MINOR: i32 = 0;
|
||||
|
||||
actions!(
|
||||
notebook,
|
||||
[
|
||||
@@ -65,17 +66,14 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
pub struct NotebookEditor {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
project: Model<Project>,
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
project: Model<Project>,
|
||||
path: ProjectPath,
|
||||
notebook_item: Model<NotebookItem>,
|
||||
|
||||
remote_id: Option<ViewId>,
|
||||
cell_list: ListState,
|
||||
|
||||
metadata: NotebookMetadata,
|
||||
nbformat: i32,
|
||||
nbformat_minor: i32,
|
||||
selected_cell_index: usize,
|
||||
cell_order: Vec<CellId>,
|
||||
cell_map: HashMap<CellId, Cell>,
|
||||
@@ -89,47 +87,23 @@ impl NotebookEditor {
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let notebook = notebook_item.read(cx).notebook.clone();
|
||||
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let language_name = notebook_item.read(cx).language_name();
|
||||
|
||||
let metadata = notebook.metadata;
|
||||
let nbformat = notebook.nbformat;
|
||||
let nbformat_minor = notebook.nbformat_minor;
|
||||
let notebook_language = notebook_item.read(cx).notebook_language();
|
||||
let notebook_language = cx.spawn(|_, _| notebook_language).shared();
|
||||
|
||||
let language_name = metadata
|
||||
.language_info
|
||||
.as_ref()
|
||||
.map(|l| l.name.clone())
|
||||
.or(metadata
|
||||
.kernelspec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.language.clone()));
|
||||
let mut cell_order = vec![]; // Vec<CellId>
|
||||
let mut cell_map = HashMap::default(); // HashMap<CellId, Cell>
|
||||
|
||||
let notebook_language = if let Some(language_name) = language_name {
|
||||
cx.spawn(|_, _| {
|
||||
let languages = languages.clone();
|
||||
async move { languages.language_for_name(&language_name).await.ok() }
|
||||
})
|
||||
.shared()
|
||||
} else {
|
||||
Task::ready(None).shared()
|
||||
};
|
||||
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let notebook_language = cx
|
||||
.spawn(|_, _| {
|
||||
// todo: pull from notebook metadata
|
||||
const TODO: &'static str = "Python";
|
||||
let languages = languages.clone();
|
||||
async move { languages.language_for_name(TODO).await.ok() }
|
||||
})
|
||||
.shared();
|
||||
|
||||
let mut cell_order = vec![];
|
||||
let mut cell_map = HashMap::default();
|
||||
|
||||
for (index, cell) in notebook.cells.iter().enumerate() {
|
||||
for (index, cell) in notebook_item
|
||||
.read(cx)
|
||||
.notebook
|
||||
.clone()
|
||||
.cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let cell_id = cell.id();
|
||||
cell_order.push(cell_id.clone());
|
||||
cell_map.insert(
|
||||
@@ -140,44 +114,35 @@ impl NotebookEditor {
|
||||
|
||||
let view = cx.view().downgrade();
|
||||
let cell_count = cell_order.len();
|
||||
let cell_order_for_list = cell_order.clone();
|
||||
let cell_map_for_list = cell_map.clone();
|
||||
|
||||
let this = cx.view();
|
||||
let cell_list = ListState::new(
|
||||
cell_count,
|
||||
gpui::ListAlignment::Top,
|
||||
// TODO: This is a totally random number,
|
||||
// not sure what this should be
|
||||
px(3000.),
|
||||
px(1000.),
|
||||
move |ix, cx| {
|
||||
let cell_order_for_list = cell_order_for_list.clone();
|
||||
let cell_id = cell_order_for_list[ix].clone();
|
||||
if let Some(view) = view.upgrade() {
|
||||
let cell_id = cell_id.clone();
|
||||
if let Some(cell) = cell_map_for_list.clone().get(&cell_id) {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_cell(ix, cell, cx).into_any_element()
|
||||
view.upgrade()
|
||||
.and_then(|notebook_handle| {
|
||||
notebook_handle.update(cx, |notebook, cx| {
|
||||
notebook
|
||||
.cell_order
|
||||
.get(ix)
|
||||
.and_then(|cell_id| notebook.cell_map.get(cell_id))
|
||||
.map(|cell| notebook.render_cell(ix, cell, cx).into_any_element())
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
project,
|
||||
languages: languages.clone(),
|
||||
focus_handle,
|
||||
project,
|
||||
path: notebook_item.read(cx).project_path.clone(),
|
||||
notebook_item,
|
||||
remote_id: None,
|
||||
cell_list,
|
||||
selected_cell_index: 0,
|
||||
metadata,
|
||||
nbformat,
|
||||
nbformat_minor,
|
||||
cell_order: cell_order.clone(),
|
||||
cell_map: cell_map.clone(),
|
||||
}
|
||||
@@ -524,10 +489,15 @@ impl FocusableView for NotebookEditor {
|
||||
}
|
||||
}
|
||||
|
||||
// Intended to be a NotebookBuffer
|
||||
pub struct NotebookItem {
|
||||
path: PathBuf,
|
||||
project_path: ProjectPath,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
// Raw notebook data
|
||||
notebook: nbformat::v4::Notebook,
|
||||
// Store our version of the notebook in memory (cell_order, cell_map)
|
||||
id: ProjectEntryId,
|
||||
}
|
||||
|
||||
impl project::Item for NotebookItem {
|
||||
@@ -538,6 +508,8 @@ impl project::Item for NotebookItem {
|
||||
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||
let path = path.clone();
|
||||
let project = project.clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let languages = project.read(cx).languages().clone();
|
||||
|
||||
if path.path.extension().unwrap_or_default() == "ipynb" {
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
@@ -545,26 +517,36 @@ impl project::Item for NotebookItem {
|
||||
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?;
|
||||
|
||||
let file_content = std::fs::read_to_string(abs_path.clone())?;
|
||||
// todo: watch for changes to the file
|
||||
let file_content = fs.load(&abs_path.as_path()).await?;
|
||||
let notebook = nbformat::parse_notebook(&file_content);
|
||||
|
||||
let notebook = match notebook {
|
||||
Ok(nbformat::Notebook::V4(notebook)) => notebook,
|
||||
// 4.1 - 4.4 are converted to 4.5
|
||||
Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
|
||||
// todo!(): Decide if we want to mutate the notebook by including Cell IDs
|
||||
// and any other conversions
|
||||
let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?;
|
||||
notebook
|
||||
}
|
||||
// Bad notebooks and notebooks v4.0 and below are not supported
|
||||
Err(e) => {
|
||||
anyhow::bail!("Failed to parse notebook: {:?}", e);
|
||||
}
|
||||
};
|
||||
|
||||
let id = project
|
||||
.update(&mut cx, |project, cx| project.entry_for_path(&path, cx))?
|
||||
.context("Entry not found")?
|
||||
.id;
|
||||
|
||||
cx.new_model(|_| NotebookItem {
|
||||
path: abs_path,
|
||||
project_path: path,
|
||||
languages,
|
||||
notebook,
|
||||
id,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
@@ -573,7 +555,7 @@ impl project::Item for NotebookItem {
|
||||
}
|
||||
|
||||
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
None
|
||||
Some(self.id)
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
@@ -581,6 +563,35 @@ impl project::Item for NotebookItem {
|
||||
}
|
||||
}
|
||||
|
||||
impl NotebookItem {
|
||||
pub fn language_name(&self) -> Option<String> {
|
||||
self.notebook
|
||||
.metadata
|
||||
.language_info
|
||||
.as_ref()
|
||||
.map(|l| l.name.clone())
|
||||
.or(self
|
||||
.notebook
|
||||
.metadata
|
||||
.kernelspec
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.language.clone()))
|
||||
}
|
||||
|
||||
pub fn notebook_language(&self) -> impl Future<Output = Option<Arc<Language>>> {
|
||||
let language_name = self.language_name();
|
||||
let languages = self.languages.clone();
|
||||
|
||||
async move {
|
||||
if let Some(language_name) = language_name {
|
||||
languages.language_for_name(&language_name).await.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for NotebookEditor {}
|
||||
|
||||
// pub struct NotebookControls {
|
||||
@@ -631,12 +642,41 @@ impl EventEmitter<()> for NotebookEditor {}
|
||||
impl Item for NotebookEditor {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
let path = self.path.path.clone();
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<gpui::View<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new_view(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), cx)))
|
||||
}
|
||||
|
||||
path.file_stem()
|
||||
.map(|stem| stem.to_string_lossy().into_owned())
|
||||
.map(SharedString::from)
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
|
||||
) {
|
||||
f(self.notebook_item.entity_id(), self.notebook_item.read(cx))
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
let path = &self.notebook_item.read(cx).path;
|
||||
let title = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Label::new(title)
|
||||
.single_line()
|
||||
.color(params.text_color())
|
||||
.italic(params.preview)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
||||
@@ -647,8 +687,54 @@ impl Item for NotebookEditor {
|
||||
false
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn can_save(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
// TODO
|
||||
fn save(
|
||||
&mut self,
|
||||
_format: bool,
|
||||
_project: Model<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("save() must be implemented if can_save() returns true")
|
||||
}
|
||||
|
||||
// TODO
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: Model<Project>,
|
||||
_path: ProjectPath,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("save_as() must be implemented if can_save() returns true")
|
||||
}
|
||||
// TODO
|
||||
fn reload(
|
||||
&mut self,
|
||||
_project: Model<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("reload() must be implemented if can_save() returns true")
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
// self.is_dirty(cx)
|
||||
// self.is_dirty(cx) TODO
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +546,9 @@ impl BufferSearchBar {
|
||||
registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
}));
|
||||
registrar.register_handler(ForDeployed(|this, _: &Dismiss, cx| {
|
||||
this.dismiss(&Dismiss, cx);
|
||||
}));
|
||||
|
||||
// register deploy buffer search for both search bar states, since we want to focus into the search bar
|
||||
// when the deploy action is triggered in the buffer.
|
||||
|
||||
@@ -2,7 +2,6 @@ use schemars::schema::{ArrayValidation, InstanceType, RootSchema, Schema, Schema
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct SettingsJsonSchemaParams<'a> {
|
||||
pub staff_mode: bool,
|
||||
pub language_names: &'a [String],
|
||||
pub font_names: &'a [String],
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl RenderOnce for ThemeControl {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
|
||||
for theme in theme_registry.list_names(false) {
|
||||
for theme in theme_registry.list_names() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let theme = theme.clone();
|
||||
|
||||
@@ -131,6 +131,8 @@ pub enum RevealStrategy {
|
||||
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
|
||||
#[default]
|
||||
Always,
|
||||
/// Always show the terminal pane, add the task's tab in it, but don't focus it.
|
||||
NoFocus,
|
||||
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
|
||||
Never,
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct EventWrapper {
|
||||
pub enum AssistantKind {
|
||||
Panel,
|
||||
Inline,
|
||||
InlineTerminal,
|
||||
}
|
||||
impl Display for AssistantKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -56,6 +57,7 @@ impl Display for AssistantKind {
|
||||
match self {
|
||||
Self::Panel => "panel",
|
||||
Self::Inline => "inline",
|
||||
Self::InlineTerminal => "inline_terminal",
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -140,6 +142,8 @@ pub struct CallEvent {
|
||||
pub struct AssistantEvent {
|
||||
/// Unique random identifier for each assistant tab (None for inline assist)
|
||||
pub conversation_id: Option<String>,
|
||||
/// Server-generated message ID (only supported for some providers)
|
||||
pub message_id: Option<String>,
|
||||
/// The kind of assistant (Panel, Inline)
|
||||
pub kind: AssistantKind,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -575,9 +575,9 @@ impl TerminalPanel {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
|
||||
fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) {
|
||||
self.pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_index, true, true, cx)
|
||||
pane.activate_item(item_index, true, focus, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -616,8 +616,14 @@ impl TerminalPanel {
|
||||
pane.add_item(terminal_view, true, focus, None, cx);
|
||||
});
|
||||
|
||||
if reveal_strategy == RevealStrategy::Always {
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
match reveal_strategy {
|
||||
RevealStrategy::Always => {
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
}
|
||||
RevealStrategy::NoFocus => {
|
||||
workspace.open_panel::<Self>(cx);
|
||||
}
|
||||
RevealStrategy::Never => {}
|
||||
}
|
||||
Ok(terminal)
|
||||
})?;
|
||||
@@ -700,7 +706,7 @@ impl TerminalPanel {
|
||||
|
||||
match reveal {
|
||||
RevealStrategy::Always => {
|
||||
self.activate_terminal_view(terminal_item_index, cx);
|
||||
self.activate_terminal_view(terminal_item_index, true, cx);
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
task_workspace
|
||||
@@ -709,6 +715,16 @@ impl TerminalPanel {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::NoFocus => {
|
||||
self.activate_terminal_view(terminal_item_index, false, cx);
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::Never => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,9 +43,8 @@ use undo_map::UndoMap;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
|
||||
static LINE_SEPARATORS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"\r\n|\r|\u{2028}|\u{2029}").expect("Failed to create LINE_SEPARATORS_REGEX")
|
||||
});
|
||||
static LINE_SEPARATORS_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\r\n|\r").expect("Failed to create LINE_SEPARATORS_REGEX"));
|
||||
|
||||
pub type TransactionId = clock::Lamport;
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
serde_repr.workspace = true
|
||||
settings.workspace = true
|
||||
strum.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user