Compare commits
100 Commits
gpui-selec
...
gpui-set-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f448d7e2e8 | ||
|
|
4624a0a9f4 | ||
|
|
bcf7bc9de8 | ||
|
|
5a7b8f7fe3 | ||
|
|
0c11d841e8 | ||
|
|
4eca7875ae | ||
|
|
843d299d9a | ||
|
|
88c4e0b2d8 | ||
|
|
a64e20ed96 | ||
|
|
f2a415135b | ||
|
|
96a3021b12 | ||
|
|
d6a6330419 | ||
|
|
da6a6ec36b | ||
|
|
f3fffc25c4 | ||
|
|
2e6d044bac | ||
|
|
530bc5c99e | ||
|
|
9edd81c740 | ||
|
|
11bc28080f | ||
|
|
fd3831861b | ||
|
|
68a0035264 | ||
|
|
9a60c0a059 | ||
|
|
5486c3dc93 | ||
|
|
3018a64a1b | ||
|
|
8633909347 | ||
|
|
091e7cb395 | ||
|
|
bb1817ff31 | ||
|
|
8871fec2a8 | ||
|
|
32b59bfa0e | ||
|
|
f658af5903 | ||
|
|
f99b24acca | ||
|
|
e4f13dd561 | ||
|
|
056c785f4e | ||
|
|
970a5957cc | ||
|
|
b25eb9afe2 | ||
|
|
0aab6d8bdc | ||
|
|
8caca6db29 | ||
|
|
d0c95c2f43 | ||
|
|
910963e5f3 | ||
|
|
237cc9b4a9 | ||
|
|
38a50a042a | ||
|
|
01aa7688c5 | ||
|
|
d4636481ac | ||
|
|
29c675ba17 | ||
|
|
bc5f82d40c | ||
|
|
80733e919d | ||
|
|
27a9498cb0 | ||
|
|
593f0e0c3e | ||
|
|
6e2be283dd | ||
|
|
cf6c2daaa2 | ||
|
|
283d424485 | ||
|
|
c68b700312 | ||
|
|
08c9157a1e | ||
|
|
1e84f01041 | ||
|
|
5a71d8c7f1 | ||
|
|
14c7782ce6 | ||
|
|
1a9b0536a2 | ||
|
|
9ec0927701 | ||
|
|
89039f6f34 | ||
|
|
6964302d89 | ||
|
|
335c307b93 | ||
|
|
02a859fb08 | ||
|
|
f576bd3aaf | ||
|
|
15299dcf80 | ||
|
|
75a545308d | ||
|
|
d62943930b | ||
|
|
0969f314b9 | ||
|
|
8d390f986d | ||
|
|
b2582a7b1b | ||
|
|
a497c49fb8 | ||
|
|
3e5dcd1bec | ||
|
|
6bdcfad6ad | ||
|
|
86696d88cf | ||
|
|
dccf6dae01 | ||
|
|
6563330239 | ||
|
|
610968815c | ||
|
|
ff56ca7280 | ||
|
|
899f7113ba | ||
|
|
beee79a9e7 | ||
|
|
b3d969ef3c | ||
|
|
55555bb41f | ||
|
|
f5e155b5a9 | ||
|
|
36055505cd | ||
|
|
9348e6f7fb | ||
|
|
f987ff05fd | ||
|
|
2306e3cd50 | ||
|
|
f252d9cf67 | ||
|
|
5ce45908b1 | ||
|
|
cd03e473c8 | ||
|
|
e69e25c171 | ||
|
|
61a60d37a2 | ||
|
|
4024b9ac4d | ||
|
|
b523ee6980 | ||
|
|
5f0046b923 | ||
|
|
155a80c6a5 | ||
|
|
7bc1025d91 | ||
|
|
b58bf64f0a | ||
|
|
47b38a0428 | ||
|
|
1915a756a0 | ||
|
|
43ad470e58 | ||
|
|
1abd58070b |
35
.github/workflows/deploy_docs.yml
vendored
Normal file
35
.github/workflows/deploy_docs.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
- name: Build book
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p target/deploy
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
164
Cargo.lock
generated
164
Cargo.lock
generated
@@ -382,7 +382,6 @@ dependencies = [
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
@@ -412,10 +411,17 @@ name = "assistant_tooling"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1481,7 +1487,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1511,7 +1517,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2280,6 +2286,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
"headless",
|
||||
@@ -2316,6 +2323,7 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -2360,6 +2368,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -2511,30 +2520,10 @@ dependencies = [
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"rpc",
|
||||
"serde",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copilot_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"copilot",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -2543,14 +2532,18 @@ dependencies = [
|
||||
"language",
|
||||
"lsp",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3409,6 +3402,7 @@ dependencies = [
|
||||
"smol",
|
||||
"snippet",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"text",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -4398,14 +4392,16 @@ name = "git"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"clock",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"git2",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4432,6 +4428,25 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git_hosting_providers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
@@ -4618,6 +4633,7 @@ dependencies = [
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"windows 0.53.0",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
@@ -5142,6 +5158,30 @@ dependencies = [
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inline_completion_button"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"copilot",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
@@ -5547,6 +5587,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"copilot",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
@@ -7421,7 +7462,6 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"copilot",
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -8704,7 +8744,12 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
@@ -9591,6 +9636,43 @@ dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"supermaven_api",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven_api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval"
|
||||
version = "2.8.0"
|
||||
@@ -9824,6 +9906,7 @@ dependencies = [
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json_lenient",
|
||||
@@ -9836,7 +9919,6 @@ dependencies = [
|
||||
name = "tasks_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
@@ -11728,6 +11810,19 @@ dependencies = [
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.2.0"
|
||||
@@ -11795,12 +11890,12 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"copilot_ui",
|
||||
"db",
|
||||
"editor",
|
||||
"extensions_ui",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"inline_completion_button",
|
||||
"install_cli",
|
||||
"picker",
|
||||
"project",
|
||||
@@ -12680,7 +12775,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"copilot",
|
||||
"copilot_ui",
|
||||
"db",
|
||||
"dev_server_projects",
|
||||
"diagnostics",
|
||||
@@ -12693,10 +12787,13 @@ dependencies = [
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"go_to_line",
|
||||
"gpui",
|
||||
"headless",
|
||||
"image_viewer",
|
||||
"inline_completion_button",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"journal",
|
||||
@@ -12727,6 +12824,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"supermaven",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
@@ -12790,7 +12888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.2"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12924,7 +13022,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12945,7 +13043,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -20,7 +20,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/copilot",
|
||||
"crates/copilot_ui",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
@@ -36,12 +35,14 @@ members = [
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/git_hosting_providers",
|
||||
"crates/go_to_line",
|
||||
"crates/google_ai",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/image_viewer",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
@@ -86,6 +87,8 @@ members = [
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/tab_switcher",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
@@ -159,7 +162,6 @@ color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
copilot_ui = { path = "crates/copilot_ui" }
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -173,6 +175,7 @@ fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
@@ -180,6 +183,7 @@ gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
@@ -220,6 +224,8 @@ settings = { path = "crates/settings" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api"}
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
@@ -250,8 +256,8 @@ async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
|
||||
cap-std = "3.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.77-bookworm as builder
|
||||
FROM rust:1.78-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
100
README.md
100
README.md
@@ -1,50 +1,50 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew install --cask zed@preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew install --cask zed@preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
8
assets/icons/supermaven.svg
Normal file
8
assets/icons/supermaven.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.30859 13.0703C3.80693 13.0703 4.21094 12.6663 4.21094 12.168C4.21094 11.6696 3.80693 11.2656 3.30859 11.2656C2.81025 11.2656 2.40625 11.6696 2.40625 12.168C2.40625 12.6663 2.81025 13.0703 3.30859 13.0703Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.53516 8.03849L4.10799 12.6055L2.51562 11.7584L4.94279 7.19141L6.53516 8.03849Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.38281 2.62443L4.93916 7.19141L3.33594 6.34432L5.77959 1.77734L7.38281 2.62443Z" fill="black"/>
|
||||
<path d="M6.5625 3.08984C7.06084 3.08984 7.46484 2.68585 7.46484 2.1875C7.46484 1.68915 7.06084 1.28516 6.5625 1.28516C6.06416 1.28516 5.66016 1.68915 5.66016 2.1875C5.66016 2.68585 6.06416 3.08984 6.5625 3.08984Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.882 1.31204C11.2842 1.41224 11.5664 1.7732 11.5664 2.18737V12.168H9.76084V5.8056L8.12938 8.87176L6.53516 8.02471L9.86653 1.76385C10.0611 1.39816 10.4799 1.21184 10.882 1.31204Z" fill="black"/>
|
||||
<path d="M10.6641 13.0703C11.1624 13.0703 11.5664 12.6663 11.5664 12.168C11.5664 11.6696 11.1624 11.2656 10.6641 11.2656C10.1657 11.2656 9.76172 11.6696 9.76172 12.168C9.76172 12.6663 10.1657 13.0703 10.6641 13.0703Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
15
assets/icons/supermaven_disabled.svg
Normal file
15
assets/icons/supermaven_disabled.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M0.906311 6.42261L1.75155 4.60999L15.3462 10.9493L14.5009 12.7619L0.906311 6.42261Z" fill="white"/>
|
||||
<circle cx="14.7841" cy="11.7906" r="1" transform="rotate(-65 14.7841 11.7906)" fill="white"/>
|
||||
<circle cx="1.32893" cy="5.51631" r="1" transform="rotate(-65 1.32893 5.51631)" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
11
assets/icons/supermaven_error.svg
Normal file
11
assets/icons/supermaven_error.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6847 15.9265C14.7823 16.0241 14.9406 16.0241 15.0382 15.9265L15.9259 15.0387C16.0235 14.9411 16.0235 14.7828 15.9259 14.6851L14.2408 12.9999L15.9259 11.3146C16.0236 11.217 16.0236 11.0587 15.9259 10.961L15.0382 10.0733C14.9406 9.97561 14.7823 9.97561 14.6847 10.0733L12.9996 11.7585L11.3145 10.0732C11.2169 9.97559 11.0586 9.97559 10.9609 10.0732L10.0732 10.961C9.97559 11.0587 9.97559 11.217 10.0732 11.3146L11.7584 12.9999L10.0732 14.6851C9.97562 14.7828 9.97562 14.9411 10.0732 15.0387L10.9609 15.9265C11.0586 16.0242 11.2169 16.0242 11.3145 15.9265L12.9996 14.2413L14.6847 15.9265Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
11
assets/icons/supermaven_init.svg
Normal file
11
assets/icons/supermaven_init.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<circle cx="13" cy="13" r="3" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -39,13 +39,13 @@
|
||||
"cmd-shift-left": "editor::SelectToBeginningOfLine",
|
||||
"cmd-shift-right": "editor::SelectToEndOfLine",
|
||||
"alt-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
"editor::SelectToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-right": [
|
||||
"editor::SelectToEndOfLine",
|
||||
"editor::SelectToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
"ctrl-q": "vim::ToggleVisualBlock",
|
||||
"shift-k": "editor::Hover",
|
||||
"shift-r": "vim::ToggleReplace",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"ctrl-f": "vim::PageDown",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"base_keymap": "VSCode",
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Show Copilot icon in status bar
|
||||
"copilot": true
|
||||
// Which inline completion provider to use.
|
||||
"inline_completion_provider": "copilot"
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -141,7 +141,7 @@ pub enum TextDelta {
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
client: &dyn HttpClient,
|
||||
client: Arc<dyn HttpClient>,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
|
||||
@@ -22,7 +22,6 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
1
crates/assistant2/evals/list-of-into-element.md
Normal file
1
crates/assistant2/evals/list-of-into-element.md
Normal file
@@ -0,0 +1 @@
|
||||
> Give me a comprehensive list of all the elements define in my project (impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})
|
||||
1
crates/assistant2/evals/new-gpui-element.md
Normal file
1
crates/assistant2/evals/new-gpui-element.md
Normal file
@@ -0,0 +1 @@
|
||||
> What are all the places we define a new gpui element in my project? (impl Element for {})
|
||||
1
crates/assistant2/evals/what-is-the-assistant2-crate.md
Normal file
1
crates/assistant2/evals/what-is-the-assistant2-crate.md
Normal file
@@ -0,0 +1 @@
|
||||
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.
|
||||
@@ -1,378 +0,0 @@
|
||||
//! This example creates a basic Chat UI with a function for rolling a die.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||
use client::{Client, UserStore};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::ResultExt as _;
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
struct RollDiceTool {}
|
||||
|
||||
impl RollDiceTool {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum Die {
|
||||
D6 = 6,
|
||||
D20 = 20,
|
||||
}
|
||||
|
||||
impl Die {
|
||||
fn into_str(&self) -> &'static str {
|
||||
match self {
|
||||
Die::D6 => "d6",
|
||||
Die::D20 => "d20",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
struct DiceParams {
|
||||
/// The number of dice to roll.
|
||||
num_dice: u8,
|
||||
/// Which die to roll. Defaults to a d6 if not provided.
|
||||
die_type: Option<Die>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DieRoll {
|
||||
die: Die,
|
||||
roll: u8,
|
||||
}
|
||||
|
||||
impl DieRoll {
|
||||
fn render(&self) -> AnyElement {
|
||||
match self.die {
|
||||
Die::D6 => {
|
||||
let face = match self.roll {
|
||||
6 => div().child("⚅"),
|
||||
5 => div().child("⚄"),
|
||||
4 => div().child("⚃"),
|
||||
3 => div().child("⚂"),
|
||||
2 => div().child("⚁"),
|
||||
1 => div().child("⚀"),
|
||||
_ => div().child("😅"),
|
||||
};
|
||||
face.text_3xl().into_any_element()
|
||||
}
|
||||
_ => div()
|
||||
.child(format!("{}", self.roll))
|
||||
.text_3xl()
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DiceRoll {
|
||||
rolls: Vec<DieRoll>,
|
||||
}
|
||||
|
||||
pub struct DiceView {
|
||||
result: Result<DiceRoll>,
|
||||
}
|
||||
|
||||
impl Render for DiceView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let output = match &self.result {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.children(
|
||||
output
|
||||
.rolls
|
||||
.iter()
|
||||
.map(|roll| div().p_2().child(roll.render())),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for RollDiceTool {
|
||||
type Input = DiceParams;
|
||||
type Output = DiceRoll;
|
||||
type View = DiceView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"roll_dice".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Rolls N many dice and returns the results.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
let rolls = (0..input.num_dice)
|
||||
.map(|_| {
|
||||
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
|
||||
|
||||
DieRoll {
|
||||
die: die_type.clone(),
|
||||
roll: rand::thread_rng().gen_range(1..=die_type as u8),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Task::ready(Ok(DiceRoll { rolls }));
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| DiceView { result })
|
||||
}
|
||||
|
||||
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let output = match output {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".to_string(),
|
||||
};
|
||||
|
||||
let mut result = String::new();
|
||||
for roll in &output.rolls {
|
||||
let die = &roll.die;
|
||||
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
struct FileBrowserTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
root_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileBrowserTool {
|
||||
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
|
||||
Self { fs, root_dir }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct FileBrowserParams {
|
||||
command: FileBrowserCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
enum FileBrowserCommand {
|
||||
Ls { path: PathBuf },
|
||||
Cat { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum FileBrowserOutput {
|
||||
Ls { entries: Vec<String> },
|
||||
Cat { content: String },
|
||||
}
|
||||
|
||||
pub struct FileBrowserView {
|
||||
result: Result<FileBrowserOutput>,
|
||||
}
|
||||
|
||||
impl Render for FileBrowserView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Ok(output) = self.result.as_ref() else {
|
||||
return h_flex().child("Failed to perform operation");
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => v_flex().children(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
|
||||
),
|
||||
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for FileBrowserTool {
|
||||
type Input = FileBrowserParams;
|
||||
type Output = FileBrowserOutput;
|
||||
type View = FileBrowserView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"file_browser".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool for browsing the filesystem.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let fs = self.fs.clone();
|
||||
let root_dir = self.root_dir.clone();
|
||||
let input = input.clone();
|
||||
|_cx| async move {
|
||||
match input.command {
|
||||
FileBrowserCommand::Ls { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let mut output = fs.read_dir(&path).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = output.next().await {
|
||||
let entry = entry?;
|
||||
entries.push(entry.display().to_string());
|
||||
}
|
||||
|
||||
Ok(FileBrowserOutput::Ls { entries })
|
||||
}
|
||||
FileBrowserCommand::Cat { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let output = fs.load(&path).await?;
|
||||
|
||||
Ok(FileBrowserOutput::Cat { content: output })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| FileBrowserView { result })
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let Ok(output) = output else {
|
||||
return "Failed to perform command: {input:?}".to_string();
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => entries.join("\n"),
|
||||
FileBrowserOutput::Cat { content } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
let fs = Arc::new(fs::RealFs::new(None));
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(RollDiceTool::new(), cx)
|
||||
.context("failed to register DummyTool")
|
||||
.log_err();
|
||||
|
||||
tool_registry
|
||||
.register(FileBrowserTool::new(fs, cwd), cx)
|
||||
.context("failed to register FileBrowserTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx.new_view(|cx| {
|
||||
AssistantPanel::new(language_registry, tool_registry, user_store, None, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
mod assistant_settings;
|
||||
mod attachments;
|
||||
mod completion_provider;
|
||||
mod tools;
|
||||
pub mod ui;
|
||||
|
||||
use crate::{
|
||||
attachments::ActiveEditorAttachmentTool,
|
||||
tools::{CreateBufferTool, ProjectIndexTool},
|
||||
ui::UserOrAssistant,
|
||||
};
|
||||
use ::ui::{div, prelude::*, Color, ViewContext};
|
||||
use anyhow::{Context, Result};
|
||||
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
|
||||
use assistant_tooling::{
|
||||
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
|
||||
};
|
||||
use client::{proto, Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use completion_provider::*;
|
||||
@@ -19,12 +27,12 @@ use gpui::{
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
||||
use rich_text::RichText;
|
||||
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
|
||||
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use ui::Composer;
|
||||
use util::{paths::EMBEDDINGS_DIR, ResultExt};
|
||||
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
|
||||
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
@@ -32,9 +40,6 @@ use workspace::{
|
||||
|
||||
pub use assistant_settings::AssistantSettings;
|
||||
|
||||
use crate::tools::{CreateBufferTool, ProjectIndexTool};
|
||||
use crate::ui::UserOrAssistant;
|
||||
|
||||
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
|
||||
@@ -81,6 +86,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|workspace, _: &DebugProjectIndex, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
let index = panel.read(cx).chat.read(cx).project_index.clone();
|
||||
let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx));
|
||||
workspace.add_item_to_center(Box::new(view), cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -105,20 +117,14 @@ impl AssistantPanel {
|
||||
(workspace.app_state().clone(), workspace.project().clone())
|
||||
})?;
|
||||
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
cx.new_view(|cx| {
|
||||
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
|
||||
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
|
||||
semantic_index.project_index(project.clone(), cx)
|
||||
});
|
||||
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(
|
||||
ProjectIndexTool::new(project_index.clone(), app_state.fs.clone()),
|
||||
cx,
|
||||
)
|
||||
.register(ProjectIndexTool::new(project_index.clone()), cx)
|
||||
.context("failed to register ProjectIndexTool")
|
||||
.log_err();
|
||||
tool_registry
|
||||
@@ -129,13 +135,16 @@ impl AssistantPanel {
|
||||
.context("failed to register CreateBufferTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
let mut attachment_store = AttachmentRegistry::new();
|
||||
attachment_store.register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
|
||||
|
||||
Self::new(
|
||||
app_state.languages.clone(),
|
||||
tool_registry,
|
||||
user_store,
|
||||
Some(project_index),
|
||||
Arc::new(tool_registry),
|
||||
Arc::new(attachment_store),
|
||||
app_state.user_store.clone(),
|
||||
project_index,
|
||||
workspace,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -145,16 +154,20 @@ impl AssistantPanel {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
attachment_store: Arc<AttachmentRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let chat = cx.new_view(|cx| {
|
||||
AssistantChat::new(
|
||||
language_registry.clone(),
|
||||
language_registry,
|
||||
tool_registry.clone(),
|
||||
attachment_store,
|
||||
user_store,
|
||||
project_index,
|
||||
workspace,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -168,8 +181,7 @@ impl Render for AssistantPanel {
|
||||
div()
|
||||
.size_full()
|
||||
.v_flex()
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(self.chat.clone())
|
||||
}
|
||||
}
|
||||
@@ -228,13 +240,16 @@ pub struct AssistantChat {
|
||||
list_state: ListState,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
composer_editor: View<Editor>,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
user_store: Model<UserStore>,
|
||||
next_message_id: MessageId,
|
||||
collapsed_messages: HashMap<MessageId, bool>,
|
||||
editing_message: Option<EditingMessage>,
|
||||
pending_completion: Option<Task<()>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
attachment_registry: Arc<AttachmentRegistry>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
struct EditingMessage {
|
||||
@@ -247,8 +262,10 @@ impl AssistantChat {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
attachment_registry: Arc<AttachmentRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
project_index: Option<Model<ProjectIndex>>,
|
||||
project_index: Model<ProjectIndex>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = CompletionProvider::get(cx).default_model();
|
||||
@@ -263,6 +280,19 @@ impl AssistantChat {
|
||||
},
|
||||
);
|
||||
|
||||
let project_index_button = cx.new_view(|cx| {
|
||||
ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx)
|
||||
});
|
||||
|
||||
let active_file_button = match workspace.upgrade() {
|
||||
Some(workspace) => {
|
||||
Some(cx.new_view(
|
||||
|cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), //
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
model,
|
||||
messages: Vec::new(),
|
||||
@@ -275,11 +305,14 @@ impl AssistantChat {
|
||||
list_state,
|
||||
user_store,
|
||||
language_registry,
|
||||
project_index_button,
|
||||
active_file_button,
|
||||
project_index,
|
||||
next_message_id: MessageId(0),
|
||||
editing_message: None,
|
||||
collapsed_messages: HashMap::default(),
|
||||
pending_completion: None,
|
||||
attachment_registry,
|
||||
tool_registry,
|
||||
}
|
||||
}
|
||||
@@ -345,7 +378,12 @@ impl AssistantChat {
|
||||
editor
|
||||
});
|
||||
composer_editor.clear(cx);
|
||||
ChatMessage::User(UserMessage { id, body })
|
||||
|
||||
ChatMessage::User(UserMessage {
|
||||
id,
|
||||
body,
|
||||
attachments: Vec::new(),
|
||||
})
|
||||
});
|
||||
self.push_message(message, cx);
|
||||
} else {
|
||||
@@ -355,6 +393,29 @@ impl AssistantChat {
|
||||
|
||||
let mode = *mode;
|
||||
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
||||
let attachments_task = this.update(&mut cx, |this, cx| {
|
||||
let attachment_store = this.attachment_registry.clone();
|
||||
attachment_store.call_all_attachment_tools(cx)
|
||||
});
|
||||
|
||||
let attachments = maybe!(async {
|
||||
let attachments_task = attachments_task?;
|
||||
let attachments = attachments_task.await?;
|
||||
|
||||
anyhow::Ok(attachments)
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Set the attachments to the _last_ user message
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
if let Some(ChatMessage::User(message)) = this.messages.last_mut() {
|
||||
message.attachments = attachments;
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Self::request_completion(
|
||||
this.clone(),
|
||||
mode,
|
||||
@@ -372,14 +433,6 @@ impl AssistantChat {
|
||||
}));
|
||||
}
|
||||
|
||||
fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = &self.project_index {
|
||||
index.update(cx, |project_index, cx| {
|
||||
project_index.debug(cx).detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_completion(
|
||||
this: WeakView<Self>,
|
||||
mode: SubmitMode,
|
||||
@@ -389,7 +442,7 @@ impl AssistantChat {
|
||||
let mut call_count = 0;
|
||||
loop {
|
||||
let complete = async {
|
||||
let completion = this.update(cx, |this, cx| {
|
||||
let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
|
||||
this.push_new_assistant_message(cx);
|
||||
|
||||
let definitions = if call_count < limit
|
||||
@@ -397,18 +450,26 @@ impl AssistantChat {
|
||||
{
|
||||
this.tool_registry.definitions()
|
||||
} else {
|
||||
&[]
|
||||
Vec::new()
|
||||
};
|
||||
call_count += 1;
|
||||
|
||||
let messages = this.completion_messages(cx);
|
||||
|
||||
CompletionProvider::get(cx).complete(
|
||||
(
|
||||
definitions,
|
||||
this.model.clone(),
|
||||
this.completion_messages(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let messages = messages.await?;
|
||||
|
||||
let completion = cx.update(|cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
model_name,
|
||||
messages,
|
||||
Vec::new(),
|
||||
1.0,
|
||||
definitions,
|
||||
tool_definitions,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -565,9 +626,9 @@ impl AssistantChat {
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.neg_mx_1()
|
||||
.mx_neg_1()
|
||||
.rounded_md()
|
||||
.border()
|
||||
.border_1()
|
||||
.border_color(theme.status().error_border)
|
||||
// .bg(theme.status().error_background)
|
||||
.text_color(theme.status().error)
|
||||
@@ -579,18 +640,27 @@ impl AssistantChat {
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
let is_first = ix == 0;
|
||||
let is_last = ix == self.messages.len() - 1;
|
||||
|
||||
let padding = Spacing::Large.rems(cx);
|
||||
|
||||
// Whenever there's a run of assistant messages, group as one Assistant UI element
|
||||
|
||||
match &self.messages[ix] {
|
||||
ChatMessage::User(UserMessage { id, body }) => div()
|
||||
ChatMessage::User(UserMessage {
|
||||
id,
|
||||
body,
|
||||
attachments,
|
||||
}) => div()
|
||||
.id(SharedString::from(format!("message-{}-container", id.0)))
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.when(is_first, |this| this.pt(padding))
|
||||
.map(|element| {
|
||||
if self.editing_message_id() == Some(*id) {
|
||||
element.child(Composer::new(
|
||||
body.clone(),
|
||||
self.user_store.read(cx).current_user(),
|
||||
self.tool_registry.clone(),
|
||||
self.project_index_button.clone(),
|
||||
self.active_file_button.clone(),
|
||||
crate::ui::ModelSelector::new(
|
||||
cx.view().downgrade(),
|
||||
self.model.clone(),
|
||||
@@ -613,25 +683,39 @@ impl AssistantChat {
|
||||
}
|
||||
}
|
||||
}))
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::User(self.user_store.read(cx).current_user()),
|
||||
Some(
|
||||
RichText::new(
|
||||
body.read(cx).text(cx),
|
||||
&[],
|
||||
&self.language_registry,
|
||||
)
|
||||
.element(ElementId::from(id.0), cx),
|
||||
),
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
.child(
|
||||
crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::User(self.user_store.read(cx).current_user()),
|
||||
Some(
|
||||
RichText::new(
|
||||
body.read(cx).text(cx),
|
||||
&[],
|
||||
&self.language_registry,
|
||||
)
|
||||
.element(ElementId::from(id.0), cx),
|
||||
),
|
||||
Some(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.children(
|
||||
attachments
|
||||
.iter()
|
||||
.map(|attachment| attachment.view.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
)
|
||||
// TODO: Wire up selections.
|
||||
.selected(is_last),
|
||||
)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
@@ -647,57 +731,65 @@ impl AssistantChat {
|
||||
} else {
|
||||
Some(
|
||||
div()
|
||||
.p_2()
|
||||
.child(body.element(ElementId::from(id.0), cx))
|
||||
.into_any_element(),
|
||||
)
|
||||
};
|
||||
|
||||
let tools = tool_calls
|
||||
.iter()
|
||||
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
|
||||
.collect::<Vec<AnyElement>>();
|
||||
|
||||
let tools_body = if tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(div().children(tools).into_any_element())
|
||||
};
|
||||
|
||||
div()
|
||||
.when(!is_last, |element| element.mb_2())
|
||||
.child(crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::Assistant,
|
||||
assistant_body,
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
))
|
||||
// TODO: Should the errors and tool calls get passed into `ChatMessage`?
|
||||
.when(is_first, |this| this.pt(padding))
|
||||
.child(
|
||||
crate::ui::ChatMessage::new(
|
||||
*id,
|
||||
UserOrAssistant::Assistant,
|
||||
assistant_body,
|
||||
tools_body,
|
||||
self.is_message_collapsed(id),
|
||||
Box::new(cx.listener({
|
||||
let id = *id;
|
||||
move |assistant_chat, _event, _cx| {
|
||||
assistant_chat.toggle_message_collapsed(id)
|
||||
}
|
||||
})),
|
||||
)
|
||||
// TODO: Wire up selections.
|
||||
.selected(is_last),
|
||||
)
|
||||
.child(self.render_error(error.clone(), ix, cx))
|
||||
.children(tool_calls.iter().map(|tool_call| {
|
||||
let result = &tool_call.result;
|
||||
let name = tool_call.name.clone();
|
||||
match result {
|
||||
Some(result) => {
|
||||
div().p_2().child(result.into_any_element(&name)).into_any()
|
||||
}
|
||||
None => div()
|
||||
.p_2()
|
||||
.child(Label::new(name).color(Color::Modified))
|
||||
.child("Running...")
|
||||
.into_any(),
|
||||
}
|
||||
}))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
|
||||
fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
let project = project_index.project();
|
||||
let fs = project_index.fs();
|
||||
|
||||
let mut project_context = ProjectContext::new(project, fs);
|
||||
let mut completion_messages = Vec::new();
|
||||
|
||||
for message in &self.messages {
|
||||
match message {
|
||||
ChatMessage::User(UserMessage { body, .. }) => {
|
||||
// When we re-introduce contexts like active file, we'll inject them here instead of relying on the model to request them
|
||||
// contexts.iter().for_each(|context| {
|
||||
// completion_messages.extend(context.completion_messages(cx))
|
||||
// });
|
||||
ChatMessage::User(UserMessage {
|
||||
body, attachments, ..
|
||||
}) => {
|
||||
for attachment in attachments {
|
||||
if let Some(content) = attachment.generate(&mut project_context, cx) {
|
||||
completion_messages.push(CompletionMessage::System { content });
|
||||
}
|
||||
}
|
||||
|
||||
// Show user's message last so that the assistant is grounded in the user's request
|
||||
completion_messages.push(CompletionMessage::User {
|
||||
@@ -732,11 +824,11 @@ impl AssistantChat {
|
||||
});
|
||||
|
||||
for tool_call in tool_calls {
|
||||
// todo!(): we should not be sending when the tool is still running / has no result
|
||||
// For now I'm going to have to assume we send an empty string because otherwise
|
||||
// the Chat API will break -- there is a required message for every tool call by ID
|
||||
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
|
||||
let content = match &tool_call.result {
|
||||
Some(result) => result.format(&tool_call.name),
|
||||
Some(result) => {
|
||||
result.generate(&tool_call.name, &mut project_context, cx)
|
||||
}
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
@@ -749,7 +841,13 @@ impl AssistantChat {
|
||||
}
|
||||
}
|
||||
|
||||
completion_messages
|
||||
let system_message = project_context.generate_system_message(cx);
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let content = system_message.await?;
|
||||
completion_messages.insert(0, CompletionMessage::System { content });
|
||||
Ok(completion_messages)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,13 +860,12 @@ impl Render for AssistantChat {
|
||||
.key_context("AssistantChat")
|
||||
.on_action(cx.listener(Self::submit))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::debug_project_index))
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(list(self.list_state.clone()).flex_1())
|
||||
.child(Composer::new(
|
||||
self.composer_editor.clone(),
|
||||
self.user_store.read(cx).current_user(),
|
||||
self.tool_registry.clone(),
|
||||
self.project_index_button.clone(),
|
||||
self.active_file_button.clone(),
|
||||
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
|
||||
.into_any_element(),
|
||||
))
|
||||
@@ -803,6 +900,7 @@ impl ChatMessage {
|
||||
struct UserMessage {
|
||||
id: MessageId,
|
||||
body: View<Editor>,
|
||||
attachments: Vec<UserAttachment>,
|
||||
}
|
||||
|
||||
struct AssistantMessage {
|
||||
|
||||
114
crates/assistant2/src/attachments.rs
Normal file
114
crates/assistant2/src/attachments.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
pub mod active_file;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
|
||||
use editor::Editor;
|
||||
use gpui::{Render, Task, View, WeakModel, WeakView};
|
||||
use language::Buffer;
|
||||
use project::ProjectPath;
|
||||
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ActiveEditorAttachment {
|
||||
buffer: WeakModel<Buffer>,
|
||||
path: Option<ProjectPath>,
|
||||
}
|
||||
|
||||
pub struct FileAttachmentView {
|
||||
output: Result<ActiveEditorAttachment>,
|
||||
}
|
||||
|
||||
impl Render for FileAttachmentView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
match &self.output {
|
||||
Ok(attachment) => {
|
||||
let filename: SharedString = attachment
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|p| p.path.file_name()?.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
// todo!(): make the button link to the actual file to open
|
||||
ButtonLike::new("file-attachment")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(ui::Icon::new(IconName::File))
|
||||
.child(filename.clone()),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
Err(err) => div().child(err.to_string()).into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for FileAttachmentView {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
if let Ok(result) = &self.output {
|
||||
if let Some(path) = &result.path {
|
||||
project.add_file(path.clone());
|
||||
return format!("current file: {}", path.path.display());
|
||||
} else if let Some(buffer) = result.buffer.upgrade() {
|
||||
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveEditorAttachmentTool {
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl ActiveEditorAttachmentTool {
|
||||
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
|
||||
Self { workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
|
||||
type Output = ActiveEditorAttachment;
|
||||
type View = FileAttachmentView;
|
||||
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
|
||||
Task::ready(maybe!({
|
||||
let active_buffer = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
|
||||
})?
|
||||
.ok_or_else(|| anyhow!("no active buffer"))?;
|
||||
|
||||
let buffer = active_buffer.read(cx);
|
||||
|
||||
if let Some(buffer) = buffer.as_singleton() {
|
||||
let path =
|
||||
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
});
|
||||
return Ok(ActiveEditorAttachment {
|
||||
buffer: buffer.downgrade(),
|
||||
path,
|
||||
});
|
||||
} else {
|
||||
Err(anyhow!("no active buffer"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| FileAttachmentView { output })
|
||||
}
|
||||
}
|
||||
1
crates/assistant2/src/attachments/active_file.rs
Normal file
1
crates/assistant2/src/attachments/active_file.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -33,7 +33,7 @@ impl CompletionProvider {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
self.0.complete(model, messages, stop, temperature, tools)
|
||||
@@ -51,7 +51,7 @@ pub trait CompletionProviderBackend: 'static {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl CompletionProviderBackend for CloudCompletionProvider {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
let client = self.client.clone();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Model, Task, View, WeakView};
|
||||
use project::Project;
|
||||
@@ -31,11 +31,9 @@ pub struct CreateBufferInput {
|
||||
language: String,
|
||||
}
|
||||
|
||||
pub struct CreateBufferOutput {}
|
||||
|
||||
impl LanguageModelTool for CreateBufferTool {
|
||||
type Input = CreateBufferInput;
|
||||
type Output = CreateBufferOutput;
|
||||
type Output = ();
|
||||
type View = CreateBufferView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
@@ -83,32 +81,39 @@ impl LanguageModelTool for CreateBufferTool {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(CreateBufferOutput {})
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match output {
|
||||
Ok(_) => format!("Created a new {} buffer", input.language),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
_output: Result<Self::Output>,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {})
|
||||
cx.new_view(|_cx| CreateBufferView {
|
||||
language: input.language,
|
||||
output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {}
|
||||
pub struct CreateBufferView {
|
||||
language: String,
|
||||
output: Result<()>,
|
||||
}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for CreateBufferView {
|
||||
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
|
||||
match &self.output {
|
||||
Ok(_) => format!("Created a new {} buffer", self.language),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use gpui::{percentage, prelude::*, Animation, AnimationExt, AnyView, Model, Task, Transformation};
|
||||
use project::Fs;
|
||||
use assistant_tooling::{LanguageModelTool, ToolOutput};
|
||||
use collections::BTreeMap;
|
||||
use gpui::{prelude::*, Model, Task};
|
||||
use project::ProjectPath;
|
||||
use schemars::JsonSchema;
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use serde::Deserialize;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use ui::{
|
||||
div, prelude::*, ButtonLike, CollapsibleContainer, Color, Icon, IconName, Indicator, Label,
|
||||
SharedString, Tooltip, WindowContext,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use std::{fmt::Write as _, ops::Range};
|
||||
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodebaseExcerpt {
|
||||
path: SharedString,
|
||||
text: SharedString,
|
||||
score: f32,
|
||||
element_id: ElementId,
|
||||
expanded: bool,
|
||||
pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
|
||||
@@ -37,21 +29,31 @@ pub struct CodebaseQuery {
|
||||
pub struct ProjectIndexView {
|
||||
input: CodebaseQuery,
|
||||
output: Result<ProjectIndexOutput>,
|
||||
element_id: ElementId,
|
||||
expanded_header: bool,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
status: Status,
|
||||
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl ProjectIndexView {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let Ok(output) = &mut self.output {
|
||||
if let Some(excerpt) = output
|
||||
.excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
excerpt.expanded = !excerpt.expanded;
|
||||
cx.notify();
|
||||
}
|
||||
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
|
||||
let element_id = ElementId::Name(nanoid::nanoid!().into());
|
||||
|
||||
Self {
|
||||
input,
|
||||
output,
|
||||
element_id,
|
||||
expanded_header: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.expanded_header = !self.expanded_header;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexView {
|
||||
@@ -67,61 +69,77 @@ impl Render for ProjectIndexView {
|
||||
Ok(output) => output,
|
||||
};
|
||||
|
||||
div()
|
||||
.v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(Label::new("Query: ").color(Color::Modified))
|
||||
.child(Label::new(query).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.children(output.excerpts.iter().map(|excerpt| {
|
||||
let element_id = excerpt.element_id.clone();
|
||||
let expanded = excerpt.expanded;
|
||||
let file_count = output.excerpts.len();
|
||||
|
||||
CollapsibleContainer::new(element_id.clone(), expanded)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::File).color(Color::Muted))
|
||||
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_expanded(element_id.clone(), cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(excerpt.text.clone()),
|
||||
)
|
||||
}))
|
||||
let header = h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(format!(
|
||||
"Read {} {}",
|
||||
file_count,
|
||||
if file_count == 1 { "file" } else { "files" }
|
||||
));
|
||||
|
||||
v_flex().gap_3().child(
|
||||
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
|
||||
.start_slot(header)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_header(cx);
|
||||
}))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.p_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.children(output.excerpts.keys().map(|path| {
|
||||
h_flex().gap_2().child(Icon::new(IconName::File)).child(
|
||||
Label::new(path.path.to_string_lossy().to_string())
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
impl ToolOutput for ProjectIndexView {
|
||||
fn generate(
|
||||
&self,
|
||||
context: &mut assistant_tooling::ProjectContext,
|
||||
_: &mut WindowContext,
|
||||
) -> String {
|
||||
match &self.output {
|
||||
Ok(output) => {
|
||||
let mut body = "found results in the following paths:\n".to_string();
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
excerpts: Vec<CodebaseExcerpt>,
|
||||
status: Status,
|
||||
for (project_path, ranges) in &output.excerpts {
|
||||
context.add_excerpts(project_path.clone(), ranges);
|
||||
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
|
||||
}
|
||||
|
||||
if output.status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
Err(err) => format!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectIndexTool {
|
||||
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
|
||||
// Listen for project index status and update the ProjectIndexTool directly
|
||||
|
||||
// TODO: setup a better description based on the user's current codebase.
|
||||
Self { project_index, fs }
|
||||
pub fn new(project_index: Model<ProjectIndex>) -> Self {
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,183 +153,57 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
let status = project_index.status();
|
||||
let results = project_index.search(
|
||||
let search = project_index.search(
|
||||
query.query.clone(),
|
||||
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let search_results = search.await?;
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let results = results.await?;
|
||||
cx.update(|cx| {
|
||||
let mut output = ProjectIndexOutput {
|
||||
status,
|
||||
excerpts: Default::default(),
|
||||
};
|
||||
|
||||
let excerpts = results.into_iter().map(|result| {
|
||||
let abs_path = result
|
||||
.worktree
|
||||
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
|
||||
let fs = fs.clone();
|
||||
for search_result in search_results {
|
||||
let path = ProjectPath {
|
||||
worktree_id: search_result.worktree.read(cx).id(),
|
||||
path: search_result.path.clone(),
|
||||
};
|
||||
|
||||
async move {
|
||||
let path = result.path.clone();
|
||||
let text = fs.load(&abs_path?).await?;
|
||||
|
||||
let mut start = result.range.start;
|
||||
let mut end = result.range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
anyhow::Ok(CodebaseExcerpt {
|
||||
element_id: ElementId::Name(nanoid::nanoid!().into()),
|
||||
expanded: false,
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
text: SharedString::from(text[start..end].to_string()),
|
||||
score: result.score,
|
||||
})
|
||||
let excerpts_for_path = output.excerpts.entry(path).or_default();
|
||||
let ix = match excerpts_for_path
|
||||
.binary_search_by_key(&search_result.range.start, |r| r.start)
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
excerpts_for_path.insert(ix, search_result.range);
|
||||
}
|
||||
});
|
||||
|
||||
let excerpts = futures::future::join_all(excerpts)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect();
|
||||
anyhow::Ok(ProjectIndexOutput { excerpts, status })
|
||||
output
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| ProjectIndexView { input, output })
|
||||
cx.new_view(|_cx| ProjectIndexView::new(input, output))
|
||||
}
|
||||
|
||||
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
|
||||
Some(
|
||||
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match &output {
|
||||
Ok(output) => {
|
||||
let mut body = "Semantic search results:\n".to_string();
|
||||
|
||||
if output.status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
if output.excerpts.is_empty() {
|
||||
body.push_str("No results found");
|
||||
return body;
|
||||
}
|
||||
|
||||
for excerpt in &output.excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
body.push_str(&excerpt.score.to_string());
|
||||
body.push_str(":\n");
|
||||
body.push_str("~~~\n");
|
||||
body.push_str(excerpt.text.as_ref());
|
||||
body.push_str("~~~\n");
|
||||
}
|
||||
body
|
||||
}
|
||||
Err(err) => format!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectIndexStatusView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
impl ProjectIndexStatusView {
|
||||
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexStatusView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
|
||||
let is_enabled = match status {
|
||||
Status::Idle => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let icon = match status {
|
||||
Status::Idle => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default),
|
||||
Status::Loading => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
Status::Scanning { .. } => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
};
|
||||
|
||||
let indicator = match status {
|
||||
Status::Idle => Some(Indicator::dot().color(Color::Success)),
|
||||
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
|
||||
Status::Loading => Some(Indicator::icon(
|
||||
Icon::new(IconName::Spinner)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)),
|
||||
};
|
||||
|
||||
ButtonLike::new("project-index")
|
||||
.disabled(!is_enabled)
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let (tooltip, meta) = match status {
|
||||
Status::Idle => (
|
||||
"Project index ready".to_string(),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
Status::Loading => ("Project index loading...".to_string(), None),
|
||||
Status::Scanning { remaining_count } => (
|
||||
"Project index scanning...".to_string(),
|
||||
Some(format!("{} remaining...", remaining_count)),
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
fn render_running(_: &mut WindowContext) -> impl IntoElement {
|
||||
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
|
||||
.start_slot("Searching code base")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
mod active_file_button;
|
||||
mod chat_message;
|
||||
mod chat_notice;
|
||||
mod composer;
|
||||
mod project_index_button;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
pub use active_file_button::*;
|
||||
pub use chat_message::*;
|
||||
pub use chat_notice::*;
|
||||
pub use composer::*;
|
||||
pub use project_index_button::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
134
crates/assistant2/src/ui/active_file_button.rs
Normal file
134
crates/assistant2/src/ui/active_file_button.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use crate::attachments::ActiveEditorAttachmentTool;
|
||||
use assistant_tooling::AttachmentRegistry;
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Subscription, View};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Status {
|
||||
ActiveFile(String),
|
||||
#[allow(dead_code)]
|
||||
NoFile,
|
||||
}
|
||||
|
||||
pub struct ActiveFileButton {
|
||||
attachment_registry: Arc<AttachmentRegistry>,
|
||||
status: Status,
|
||||
#[allow(dead_code)]
|
||||
workspace_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ActiveFileButton {
|
||||
pub fn new(
|
||||
attachment_store: Arc<AttachmentRegistry>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event);
|
||||
|
||||
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
|
||||
|
||||
Self {
|
||||
attachment_registry: attachment_store,
|
||||
status: Status::NoFile,
|
||||
workspace_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.attachment_registry
|
||||
.set_attachment_tool_enabled::<ActiveEditorAttachmentTool>(enabled);
|
||||
}
|
||||
|
||||
pub fn update_active_buffer(&mut self, workspace: View<Workspace>, cx: &mut ViewContext<Self>) {
|
||||
let active_buffer = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()));
|
||||
|
||||
if let Some(buffer) = active_buffer {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
if let Some(singleton) = buffer.as_singleton() {
|
||||
let singleton = singleton.read(cx);
|
||||
|
||||
let filename: String = singleton
|
||||
.file()
|
||||
.map(|file| file.path().to_string_lossy())
|
||||
.unwrap_or("Untitled".into())
|
||||
.into();
|
||||
|
||||
self.status = Status::ActiveFile(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: View<Workspace>,
|
||||
event: &workspace::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let workspace::Event::ActiveItemChanged = event {
|
||||
self.update_active_buffer(workspace, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveFileButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_enabled = self
|
||||
.attachment_registry
|
||||
.is_attachment_tool_enabled::<ActiveEditorAttachmentTool>();
|
||||
|
||||
let icon = if is_enabled {
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default)
|
||||
} else {
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled)
|
||||
};
|
||||
|
||||
let indicator = None;
|
||||
|
||||
let status = self.status.clone();
|
||||
|
||||
ButtonLike::new("active-file-button")
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let status = status.clone();
|
||||
let (tooltip, meta) = match (is_enabled, status) {
|
||||
(false, _) => (
|
||||
"Active file disabled".to_string(),
|
||||
Some("Click to enable".to_string()),
|
||||
),
|
||||
(true, Status::ActiveFile(filename)) => (
|
||||
format!("Active file {filename} enabled"),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
(true, Status::NoFile) => {
|
||||
("No file active for conversation".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.set_enabled(!is_enabled);
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::User;
|
||||
use gpui::{AnyElement, ClickEvent};
|
||||
use ui::{prelude::*, Avatar};
|
||||
use gpui::{hsla, AnyElement, ClickEvent};
|
||||
use ui::{prelude::*, Avatar, Tooltip};
|
||||
|
||||
use crate::MessageId;
|
||||
|
||||
@@ -16,6 +16,8 @@ pub struct ChatMessage {
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: Option<AnyElement>,
|
||||
tools_used: Option<AnyElement>,
|
||||
selected: bool,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
}
|
||||
@@ -25,6 +27,7 @@ impl ChatMessage {
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: Option<AnyElement>,
|
||||
tools_used: Option<AnyElement>,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
) -> Self {
|
||||
@@ -32,75 +35,37 @@ impl ChatMessage {
|
||||
id,
|
||||
player,
|
||||
message,
|
||||
tools_used,
|
||||
selected: false,
|
||||
collapsed,
|
||||
on_collapse_handle_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ChatMessage {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessage {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
|
||||
let collapse_handle = h_flex()
|
||||
.id(collapse_handle_id.clone())
|
||||
.group(collapse_handle_id.clone())
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.w_1()
|
||||
.mx_2()
|
||||
.h_full()
|
||||
.on_click(self.on_collapse_handle_click)
|
||||
.child(
|
||||
div()
|
||||
.w_px()
|
||||
.h_full()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.group_hover(collapse_handle_id, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
}),
|
||||
);
|
||||
let message_group = SharedString::from(format!("{}_group", self.id.0));
|
||||
|
||||
let content_padding = rems(1.);
|
||||
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
|
||||
|
||||
let content_padding = Spacing::Small.rems(cx);
|
||||
// Clamp the message height to exactly 1.5 lines when collapsed.
|
||||
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
|
||||
|
||||
let content = self.message.map(|message| {
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.p(content_padding)
|
||||
.rounded_lg()
|
||||
.when(self.collapsed, |this| this.h(collapsed_height))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(message)
|
||||
});
|
||||
let background_color = if let UserOrAssistant::User(_) = &self.player {
|
||||
Some(cx.theme().colors().surface_background)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(ChatMessageHeader::new(self.player))
|
||||
.child(h_flex().gap_3().child(collapse_handle).children(content))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ChatMessageHeader {
|
||||
player: UserOrAssistant,
|
||||
contexts: Vec<()>,
|
||||
}
|
||||
|
||||
impl ChatMessageHeader {
|
||||
fn new(player: UserOrAssistant) -> Self {
|
||||
Self {
|
||||
player,
|
||||
contexts: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessageHeader {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let (username, avatar_uri) = match self.player {
|
||||
UserOrAssistant::Assistant => (
|
||||
"Assistant".into(),
|
||||
@@ -112,23 +77,77 @@ impl RenderOnce for ChatMessageHeader {
|
||||
UserOrAssistant::User(None) => ("You".into(), None),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.justify_between()
|
||||
v_flex()
|
||||
.group(message_group.clone())
|
||||
.gap(Spacing::XSmall.rems(cx))
|
||||
.p(Spacing::XSmall.rems(cx))
|
||||
.when(self.selected, |element| {
|
||||
element.bg(hsla(0.6, 0.67, 0.46, 0.12))
|
||||
})
|
||||
.rounded_lg()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.map(|this| {
|
||||
let avatar_size = rems_from_px(20.);
|
||||
if let Some(avatar_uri) = avatar_uri {
|
||||
this.child(Avatar::new(avatar_uri).size(avatar_size))
|
||||
} else {
|
||||
this.child(div().size(avatar_size))
|
||||
}
|
||||
})
|
||||
.child(Label::new(username).color(Color::Default)),
|
||||
.justify_between()
|
||||
.px(content_padding)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
let avatar_size = rems_from_px(20.);
|
||||
if let Some(avatar_uri) = avatar_uri {
|
||||
this.child(Avatar::new(avatar_uri).size(avatar_size))
|
||||
} else {
|
||||
this.child(div().size(avatar_size))
|
||||
}
|
||||
})
|
||||
.child(Label::new(username).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
h_flex().visible_on_hover(message_group).child(
|
||||
// temp icons
|
||||
IconButton::new(
|
||||
collapse_handle_id.clone(),
|
||||
if self.collapsed {
|
||||
IconName::ArrowUp
|
||||
} else {
|
||||
IconName::ArrowDown
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(self.on_collapse_handle_click)
|
||||
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
|
||||
), // .child(
|
||||
// IconButton::new("copy-message", IconName::Copy)
|
||||
// .icon_color(Color::Muted)
|
||||
// .icon_size(IconSize::XSmall),
|
||||
// )
|
||||
// .child(
|
||||
// IconButton::new("menu", IconName::Ellipsis)
|
||||
// .icon_color(Color::Muted)
|
||||
// .icon_size(IconSize::XSmall),
|
||||
// ),
|
||||
),
|
||||
)
|
||||
.child(div().when(!self.contexts.is_empty(), |this| {
|
||||
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
|
||||
}))
|
||||
.when(self.message.is_some() || self.tools_used.is_some(), |el| {
|
||||
el.child(
|
||||
h_flex().child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.p(content_padding)
|
||||
.gap_3()
|
||||
.text_ui(cx)
|
||||
.rounded_lg()
|
||||
.when_some(background_color, |this, background_color| {
|
||||
this.bg(background_color)
|
||||
})
|
||||
.when(self.collapsed, |this| this.h(collapsed_height))
|
||||
.children(self.message)
|
||||
.when_some(self.tools_used, |this, tools_used| this.child(tools_used)),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,63 @@
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use client::User;
|
||||
use crate::{
|
||||
ui::{ActiveFileButton, ProjectIndexButton},
|
||||
AssistantChat, CompletionProvider,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
|
||||
|
||||
use crate::{AssistantChat, CompletionProvider};
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Composer {
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
model_selector: AnyElement,
|
||||
}
|
||||
|
||||
impl Composer {
|
||||
pub fn new(
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
model_selector: AnyElement,
|
||||
) -> Self {
|
||||
Self {
|
||||
editor,
|
||||
player,
|
||||
tool_registry,
|
||||
project_index_button,
|
||||
active_file_button,
|
||||
model_selector,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex().child(self.project_index_button.clone())
|
||||
}
|
||||
|
||||
fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex().children(
|
||||
self.active_file_button
|
||||
.clone()
|
||||
.map(|view| view.into_any_element()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Composer {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let mut player_avatar = div().size(rems_from_px(20.)).into_any_element();
|
||||
if let Some(player) = self.player.clone() {
|
||||
player_avatar = Avatar::new(player.avatar_uri.clone())
|
||||
.size(rems_from_px(20.))
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let font_size = rems(0.875);
|
||||
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
|
||||
h_flex()
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.w_full()
|
||||
.items_start()
|
||||
.mt_4()
|
||||
.gap_3()
|
||||
.child(player_avatar)
|
||||
.child(
|
||||
v_flex().size_full().gap_1().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.p_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
.child(
|
||||
@@ -95,9 +97,15 @@ impl RenderOnce for Composer {
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(h_flex().gap_1().children(
|
||||
self.tool_registry.status_views().iter().cloned(),
|
||||
))
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_tools(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(self.render_attachment_tools(cx)),
|
||||
),
|
||||
)
|
||||
.child(h_flex().gap_1().child(self.model_selector)),
|
||||
),
|
||||
),
|
||||
@@ -136,7 +144,7 @@ impl RenderOnce for ModelSelector {
|
||||
let assistant_chat = self.assistant_chat.clone();
|
||||
move |cx| {
|
||||
_ = assistant_chat.update(cx, |assistant_chat, cx| {
|
||||
assistant_chat.model = model.clone();
|
||||
assistant_chat.model.clone_from(&model);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
112
crates/assistant2/src/ui/project_index_button.rs
Normal file
112
crates/assistant2/src/ui/project_index_button.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use gpui::{percentage, prelude::*, Animation, AnimationExt, Model, Transformation};
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Indicator, Tooltip};
|
||||
|
||||
use crate::tools::ProjectIndexTool;
|
||||
|
||||
pub struct ProjectIndexButton {
|
||||
project_index: Model<ProjectIndex>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
}
|
||||
|
||||
impl ProjectIndexButton {
|
||||
pub fn new(
|
||||
project_index: Model<ProjectIndex>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
project_index,
|
||||
tool_registry,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.tool_registry
|
||||
.set_tool_enabled::<ProjectIndexTool>(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
let is_enabled = self.tool_registry.is_tool_enabled::<ProjectIndexTool>();
|
||||
|
||||
let icon = if is_enabled {
|
||||
match status {
|
||||
Status::Idle => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default),
|
||||
Status::Loading => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
Status::Scanning { .. } => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
}
|
||||
} else {
|
||||
Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled)
|
||||
};
|
||||
|
||||
let indicator = if is_enabled {
|
||||
match status {
|
||||
Status::Idle => Some(Indicator::dot().color(Color::Success)),
|
||||
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
|
||||
Status::Loading => Some(Indicator::icon(
|
||||
Icon::new(IconName::Spinner)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ButtonLike::new("project-index")
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let (tooltip, meta) = match (is_enabled, status) {
|
||||
(false, _) => (
|
||||
"Project index disabled".to_string(),
|
||||
Some("Click to enable".to_string()),
|
||||
),
|
||||
(_, Status::Idle) => (
|
||||
"Project index ready".to_string(),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
(_, Status::Loading) => ("Project index loading...".to_string(), None),
|
||||
(_, Status::Scanning { remaining_count }) => (
|
||||
"Project index scanning...".to_string(),
|
||||
Some(format!("{} remaining...", remaining_count)),
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.set_enabled(!is_enabled);
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What can I do here?").into_any_element()),
|
||||
None,
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -39,6 +40,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What can I do here?").into_any_element()),
|
||||
None,
|
||||
true,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -52,6 +54,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child("You can talk to me!").into_any_element()),
|
||||
None,
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -62,6 +65,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child(MULTI_LINE_MESSAGE).into_any_element()),
|
||||
None,
|
||||
true,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -76,6 +80,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What is Rust??").into_any_element()),
|
||||
None,
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
))
|
||||
@@ -83,6 +88,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
|
||||
None,
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
))
|
||||
@@ -90,6 +96,7 @@ impl Render for ChatMessageStory {
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1)),
|
||||
Some(div().child("Sounds pretty cool!").into_any_element()),
|
||||
None,
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
)),
|
||||
|
||||
@@ -13,10 +13,18 @@ path = "src/assistant_tooling.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
pub mod registry;
|
||||
pub mod tool;
|
||||
mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use crate::registry::ToolRegistry;
|
||||
pub use crate::tool::{LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition};
|
||||
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
|
||||
};
|
||||
|
||||
148
crates/assistant_tooling/src/attachment_registry.rs
Normal file
148
crates/assistant_tooling/src/attachment_registry.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use crate::{ProjectContext, ToolOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: 'static;
|
||||
type View: Render + ToolOutput;
|
||||
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_attachments: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let call = Box::new(move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let view = cx.update(|cx| A::view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
call,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
fn generate<T: LanguageModelAttachment>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.store(is_enabled, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.load(SeqCst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<UserAttachment>> {
|
||||
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
|
||||
return Task::ready(Err(anyhow!("no attachment tool")));
|
||||
};
|
||||
|
||||
(attachment.call)(cx)
|
||||
}
|
||||
|
||||
pub fn call_all_attachment_tools(
|
||||
self: Arc<Self>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Task<Result<Vec<UserAttachment>>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let attachment_tasks = cx.update(|cx| {
|
||||
let mut tasks = Vec::new();
|
||||
for attachment in this
|
||||
.registered_attachments
|
||||
.values()
|
||||
.filter(|attachment| attachment.enabled.load(SeqCst))
|
||||
{
|
||||
tasks.push((attachment.call)(cx))
|
||||
}
|
||||
|
||||
tasks
|
||||
})?;
|
||||
|
||||
let attachments = join_all(attachment_tasks.into_iter()).await;
|
||||
|
||||
Ok(attachments
|
||||
.into_iter()
|
||||
.filter_map(|attachment| attachment.log_err())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
|
||||
let result = (self.generate_fn)(self.view.clone(), output, cx);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
296
crates/assistant_tooling/src/project_context.rs
Normal file
296
crates/assistant_tooling/src/project_context.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct ProjectContext {
|
||||
files: TreeMap<ProjectPath, PathState>,
|
||||
project: WeakModel<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PathState {
|
||||
PathOnly,
|
||||
EntireFile,
|
||||
Excerpts { ranges: Vec<Range<usize>> },
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
|
||||
Self {
|
||||
files: TreeMap::default(),
|
||||
fs,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, project_path: ProjectPath) {
|
||||
if self.files.get(&project_path).is_none() {
|
||||
self.files.insert(project_path, PathState::PathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
|
||||
let previous_state = self
|
||||
.files
|
||||
.get(&project_path)
|
||||
.unwrap_or(&PathState::PathOnly);
|
||||
|
||||
let mut ranges = match previous_state {
|
||||
PathState::EntireFile => return,
|
||||
PathState::PathOnly => Vec::new(),
|
||||
PathState::Excerpts { ranges } => ranges.to_vec(),
|
||||
};
|
||||
|
||||
for new_range in new_ranges {
|
||||
let ix = ranges.binary_search_by(|probe| {
|
||||
if probe.end < new_range.start {
|
||||
Ordering::Less
|
||||
} else if probe.start > new_range.end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
|
||||
match ix {
|
||||
Ok(mut ix) => {
|
||||
let existing = &mut ranges[ix];
|
||||
existing.start = existing.start.min(new_range.start);
|
||||
existing.end = existing.end.max(new_range.end);
|
||||
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
|
||||
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
|
||||
ranges.remove(ix + 1);
|
||||
}
|
||||
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
|
||||
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
|
||||
ranges.remove(ix - 1);
|
||||
ix -= 1;
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
ranges.insert(ix, new_range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files
|
||||
.insert(project_path, PathState::Excerpts { ranges });
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, project_path: ProjectPath) {
|
||||
self.files.insert(project_path, PathState::EntireFile);
|
||||
}
|
||||
|
||||
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
|
||||
let project = self
|
||||
.project
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("project dropped"));
|
||||
let files = self.files.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let project = project?;
|
||||
let mut result = "project structure:\n".to_string();
|
||||
|
||||
let mut last_worktree: Option<Model<Worktree>> = None;
|
||||
for (project_path, path_state) in files.iter() {
|
||||
if let Some(worktree) = &last_worktree {
|
||||
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
|
||||
last_worktree = None;
|
||||
}
|
||||
}
|
||||
|
||||
let worktree;
|
||||
if let Some(last_worktree) = &last_worktree {
|
||||
worktree = last_worktree.clone();
|
||||
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
})? {
|
||||
worktree = tree;
|
||||
last_worktree = Some(worktree.clone());
|
||||
let worktree_name =
|
||||
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
|
||||
writeln!(&mut result, "# {}", worktree_name).unwrap();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
|
||||
let path = &project_path.path;
|
||||
writeln!(&mut result, "## {}", path.display()).unwrap();
|
||||
|
||||
match path_state {
|
||||
PathState::PathOnly => {}
|
||||
PathState::EntireFile => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
|
||||
}
|
||||
PathState::Excerpts { ranges } => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
|
||||
// Assumption: ranges are in order, not overlapping
|
||||
let mut prev_range_end = 0;
|
||||
for range in ranges {
|
||||
if range.start > prev_range_end {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
prev_range_end = range.end;
|
||||
}
|
||||
|
||||
let mut start = range.start;
|
||||
let mut end = range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
result.push_str(&text[start..end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if prev_range_end < text.len() {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_message_generation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let file_3_contents = r#"
|
||||
fn test1() {}
|
||||
fn test2() {}
|
||||
fn test3() {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"root1": {
|
||||
"lib": {
|
||||
"file1.rs": "mod example;",
|
||||
"file2.rs": "",
|
||||
},
|
||||
"test": {
|
||||
"file3.rs": file_3_contents,
|
||||
}
|
||||
},
|
||||
"root2": {
|
||||
"src": {
|
||||
"main.rs": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
["/code/root1".as_ref(), "/code/root2".as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut ax = ProjectContext::new(project.downgrade(), fs);
|
||||
|
||||
ax.add_file(ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("lib/file1.rs").into(),
|
||||
});
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
|
||||
ax.add_excerpts(
|
||||
ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("test/file3.rs").into(),
|
||||
},
|
||||
&[
|
||||
file_3_contents.find("fn test2").unwrap()
|
||||
..file_3_contents.find("fn test3").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
## test/file3.rs
|
||||
~~~
|
||||
...
|
||||
fn test2() {}
|
||||
...
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyView, Task, WindowContext};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::tool::{
|
||||
LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition,
|
||||
};
|
||||
|
||||
pub struct ToolRegistry {
|
||||
tools: HashMap<
|
||||
String,
|
||||
Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
|
||||
>,
|
||||
definitions: Vec<ToolFunctionDefinition>,
|
||||
status_views: Vec<AnyView>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tools: HashMap::new(),
|
||||
definitions: Vec::new(),
|
||||
status_views: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> &[ToolFunctionDefinition] {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(
|
||||
&mut self,
|
||||
tool: T,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
self.definitions.push(tool.definition());
|
||||
|
||||
if let Some(tool_view) = tool.status_view(cx) {
|
||||
self.status_views.push(tool_view);
|
||||
}
|
||||
|
||||
let name = tool.name();
|
||||
let previous = self.tools.insert(
|
||||
name.clone(),
|
||||
// registry.call(tool_call, cx)
|
||||
Box::new(
|
||||
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::ParsingFailed),
|
||||
}));
|
||||
};
|
||||
|
||||
let result = tool.execute(&input, cx);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<T::Output> = result.await;
|
||||
let for_model = T::format(&input, &result);
|
||||
let view = cx.update(|cx| T::output_view(id.clone(), input, result, cx))?;
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::Finished {
|
||||
view: view.into(),
|
||||
for_model,
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
|
||||
pub fn call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<ToolFunctionCall>> {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let tool = match self.tools.get(&name) {
|
||||
Some(tool) => tool,
|
||||
None => {
|
||||
let name = name.clone();
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::NoSuchTool),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
tool(tool_call, cx)
|
||||
}
|
||||
|
||||
pub fn status_views(&self) -> &[AnyView] {
|
||||
&self.status_views
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::schema_for;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
result: WeatherResult,
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
div().child(format!("temperature: {}", self.result.temperature))
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type Input = WeatherQuery;
|
||||
type Output = WeatherResult;
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
Task::ready(Ok(weather))
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| {
|
||||
let result = result.unwrap();
|
||||
WeatherView { result }
|
||||
})
|
||||
}
|
||||
|
||||
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
serde_json::to_string(&output.as_ref().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let tool = WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let tools = vec![tool.definition()];
|
||||
assert_eq!(tools.len(), 1);
|
||||
|
||||
let expected = ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: schema_for!(WeatherQuery),
|
||||
};
|
||||
|
||||
assert_eq!(tools[0].name, expected.name);
|
||||
assert_eq!(tools[0].description, expected.description);
|
||||
|
||||
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_schema,
|
||||
json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
})
|
||||
);
|
||||
|
||||
let args = json!({
|
||||
"location": "San Francisco",
|
||||
"unit": "Celsius"
|
||||
});
|
||||
|
||||
let query: WeatherQuery = serde_json::from_value(args).unwrap();
|
||||
|
||||
let result = cx.update(|cx| tool.execute(&query, cx)).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result, tool.current_weather);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{AnyElement, AnyView, IntoElement as _, Render, Task, View, WindowContext};
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
#[serde(skip)]
|
||||
pub result: Option<ToolFunctionCallResult>,
|
||||
}
|
||||
|
||||
pub enum ToolFunctionCallResult {
|
||||
NoSuchTool,
|
||||
ParsingFailed,
|
||||
Finished { for_model: String, view: AnyView },
|
||||
}
|
||||
|
||||
impl ToolFunctionCallResult {
|
||||
pub fn format(&self, name: &String) -> String {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Unable to parse arguments for {name}")
|
||||
}
|
||||
ToolFunctionCallResult::Finished { for_model, .. } => for_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_any_element(&self, name: &String) -> AnyElement {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => {
|
||||
format!("Language Model attempted to call {name}").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Language Model called {name} with bad arguments").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type Output: 'static;
|
||||
|
||||
type View: Render;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(Self::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the tool with the given input.
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String;
|
||||
|
||||
fn output_view(
|
||||
tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View>;
|
||||
|
||||
fn status_view(&self, _cx: &mut WindowContext) -> Option<AnyView> {
|
||||
None
|
||||
}
|
||||
}
|
||||
431
crates/assistant_tooling/src/tool_registry.rs
Normal file
431
crates/assistant_tooling/src/tool_registry.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{
|
||||
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
|
||||
};
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
use crate::ProjectContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
#[serde(skip)]
|
||||
pub result: Option<ToolFunctionCallResult>,
|
||||
}
|
||||
|
||||
pub enum ToolFunctionCallResult {
|
||||
NoSuchTool,
|
||||
ParsingFailed,
|
||||
Finished {
|
||||
view: AnyView,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type Output: 'static;
|
||||
|
||||
type View: Render + ToolOutput;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(Self::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the tool with the given input.
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn output_view(
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View>;
|
||||
|
||||
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToolOutput: Sized {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
|
||||
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
tool.enabled.store(is_enabled, SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
return tool.enabled.load(SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
|
||||
self.registered_tools
|
||||
.values()
|
||||
.filter(|tool| tool.enabled.load(SeqCst))
|
||||
.map(|tool| tool.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
match &tool_call.result {
|
||||
Some(result) => div()
|
||||
.p_2()
|
||||
.child(result.into_any_element(&tool_call.name))
|
||||
.into_any_element(),
|
||||
None => self
|
||||
.registered_tools
|
||||
.get(&tool_call.name)
|
||||
.map(|tool| (tool.render_running)(cx))
|
||||
.unwrap_or_else(|| div().into_any_element()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(
|
||||
&mut self,
|
||||
tool: T,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
call: Box::new(
|
||||
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::ParsingFailed),
|
||||
}));
|
||||
};
|
||||
|
||||
let result = tool.execute(&input, cx);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<T::Output> = result.await;
|
||||
let view = cx.update(|cx| T::output_view(input, result, cx))?;
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::Finished {
|
||||
view: view.into(),
|
||||
generate_fn: generate::<T>,
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
render_running: render_running::<T>,
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
|
||||
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
|
||||
T::render_running(cx).into_any_element()
|
||||
}
|
||||
|
||||
fn generate<T: LanguageModelTool>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
|
||||
pub fn call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<ToolFunctionCall>> {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let tool = match self.registered_tools.get(&name) {
|
||||
Some(tool) => tool,
|
||||
None => {
|
||||
let name = name.clone();
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::NoSuchTool),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
(tool.call)(tool_call, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolFunctionCallResult {
|
||||
pub fn generate(
|
||||
&self,
|
||||
name: &String,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Unable to parse arguments for {name}")
|
||||
}
|
||||
ToolFunctionCallResult::Finished { generate_fn, view } => {
|
||||
(generate_fn)(view.clone(), project, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_any_element(&self, name: &String) -> AnyElement {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => {
|
||||
format!("Language Model attempted to call {name}").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Language Model called {name} with bad arguments").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::schema_for;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
result: WeatherResult,
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
div().child(format!("temperature: {}", self.result.temperature))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for WeatherView {
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut WindowContext) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type Input = WeatherQuery;
|
||||
type Output = WeatherResult;
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
Task::ready(Ok(weather))
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| {
|
||||
let result = result.unwrap();
|
||||
WeatherView { result }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let tool = WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let tools = vec![tool.definition()];
|
||||
assert_eq!(tools.len(), 1);
|
||||
|
||||
let expected = ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: schema_for!(WeatherQuery),
|
||||
};
|
||||
|
||||
assert_eq!(tools[0].name, expected.name);
|
||||
assert_eq!(tools[0].description, expected.description);
|
||||
|
||||
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_schema,
|
||||
json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
})
|
||||
);
|
||||
|
||||
let args = json!({
|
||||
"location": "San Francisco",
|
||||
"unit": "Celsius"
|
||||
});
|
||||
|
||||
let query: WeatherQuery = serde_json::from_value(args).unwrap();
|
||||
|
||||
let result = cx.update(|cx| tool.execute(&query, cx)).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result, tool.current_weather);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPrevi
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
@@ -24,6 +24,7 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -340,9 +341,15 @@ impl AutoUpdater {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
|
||||
let asset = match OS {
|
||||
"linux" => format!("zed-linux-{}.tar.gz", ARCH),
|
||||
"macos" => "Zed.dmg".into(),
|
||||
_ => return Err(anyhow!("auto-update not supported for OS {:?}", OS)),
|
||||
};
|
||||
|
||||
let mut url_string = client.build_url(&format!(
|
||||
"/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
|
||||
OS, ARCH
|
||||
"/api/releases/latest?asset={}&os={}&arch={}",
|
||||
asset, OS, ARCH
|
||||
));
|
||||
cx.update(|cx| {
|
||||
if let Some(param) = ReleaseChannel::try_global(cx)
|
||||
@@ -361,6 +368,7 @@ impl AutoUpdater {
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading release")?;
|
||||
|
||||
let release: JsonRelease =
|
||||
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
|
||||
|
||||
@@ -389,81 +397,18 @@ impl AutoUpdater {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-auto-update")
|
||||
.tempdir()?;
|
||||
let dmg_path = temp_dir.path().join("Zed.dmg");
|
||||
let mount_path = temp_dir.path().join("Zed");
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
mounted_app_path.push("/");
|
||||
|
||||
let mut dmg_file = File::create(&dmg_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel = ReleaseChannel::try_global(cx)
|
||||
.map(|release_channel| release_channel.display_name());
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", dmg_path);
|
||||
let downloaded_asset = download_release(&temp_dir, release, &asset, client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&dmg_path)
|
||||
.arg("-mountroot")
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to mount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to copy app: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to unmount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
match OS {
|
||||
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
@@ -471,6 +416,7 @@ impl AutoUpdater {
|
||||
this.status = AutoUpdateStatus::Updated;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -504,3 +450,150 @@ impl AutoUpdater {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_release(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
release: JsonRelease,
|
||||
target_filename: &str,
|
||||
client: Arc<HttpClientWithUrl>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
let target_path = temp_dir.path().join(target_filename);
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", target_path);
|
||||
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
async fn install_release_linux(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_tar_gz: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
.await
|
||||
.context("failed to create directory into which to extract update")?;
|
||||
|
||||
let output = Command::new("tar")
|
||||
.arg("-xzf")
|
||||
.arg(&downloaded_tar_gz)
|
||||
.arg("-C")
|
||||
.arg(&extracted)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to extract {:?} to {:?}: {:?}",
|
||||
downloaded_tar_gz,
|
||||
extracted,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let suffix = if channel != "stable" {
|
||||
format!("-{}", channel)
|
||||
} else {
|
||||
String::default()
|
||||
};
|
||||
let app_folder_name = format!("zed{}.app", suffix);
|
||||
|
||||
let from = extracted.join(&app_folder_name);
|
||||
let to = home_dir.join(".local");
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to copy Zed update from {:?} to {:?}: {:?}",
|
||||
from,
|
||||
to,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_release_macos(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_dmg: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
|
||||
let mount_path = temp_dir.path().join("Zed");
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to mount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to copy app: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to unount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Settings for ClientSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let mut result = sources.json_merge::<Self>()?;
|
||||
if let Some(server_url) = &*ZED_SERVER_URL {
|
||||
result.server_url = server_url.clone()
|
||||
result.server_url.clone_from(&server_url)
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.metrics_id.clone_from(&metrics_id);
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
}
|
||||
@@ -445,15 +445,12 @@ impl Telemetry {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state
|
||||
.app_metadata
|
||||
.app_version
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
os_name: state.app_metadata.os_name.to_string(),
|
||||
app_version: state.app_metadata.version.unwrap_or_default().to_string(),
|
||||
os_name: state.app_metadata.os.name.to_string(),
|
||||
os_version: state
|
||||
.app_metadata
|
||||
.os_version
|
||||
.os
|
||||
.version
|
||||
.map(|version| version.to_string()),
|
||||
architecture: state.architecture.to_string(),
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
prost.workspace = true
|
||||
@@ -82,6 +83,7 @@ env_logger.workspace = true
|
||||
file_finder.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -172,6 +172,11 @@ spec:
|
||||
secretKeyRef:
|
||||
name: slack
|
||||
key: panics_webhook
|
||||
- name: SUPERMAVEN_ADMIN_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: supermaven
|
||||
key: api_key
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE projects DROP COLUMN remote_project_id;
|
||||
DROP TABLE remote_projects;
|
||||
@@ -116,13 +116,6 @@ struct CreateUserParams {
|
||||
invite_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct CreateUserResponse {
|
||||
user: User,
|
||||
signup_device_id: Option<String>,
|
||||
metrics_id: String,
|
||||
}
|
||||
|
||||
async fn get_rpc_server_snapshot(
|
||||
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
|
||||
) -> Result<ErasedJson> {
|
||||
|
||||
2
crates/collab/src/completion.rs
Normal file
2
crates/collab/src/completion.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use rpc::proto;
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use rpc::{proto, ConnectionId};
|
||||
use rpc::{
|
||||
proto::{self},
|
||||
ConnectionId,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||
ModelTrait, QueryFilter,
|
||||
@@ -35,24 +38,33 @@ impl Database {
|
||||
dev_server_id: DevServerId,
|
||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
||||
self.transaction(|tx| async move {
|
||||
let servers = dev_server_project::Entity::find()
|
||||
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
|
||||
.find_also_related(project::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.collect())
|
||||
self.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_projects_for_dev_server_internal(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
||||
let servers = dev_server_project::Entity::find()
|
||||
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
|
||||
.find_also_related(project::Entity)
|
||||
.all(tx)
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn dev_server_project_ids_for_user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -136,6 +148,39 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
dev_server_id: DevServerId,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(|tx| async move {
|
||||
project::Entity::delete_many()
|
||||
.filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected != 1 {
|
||||
return Err(anyhow!(
|
||||
"no dev server project with id {}",
|
||||
dev_server_project_id
|
||||
))?;
|
||||
}
|
||||
|
||||
let status = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
let projects = self
|
||||
.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
||||
.await?;
|
||||
Ok((projects, status))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn share_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
|
||||
@@ -77,10 +77,14 @@ impl Database {
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(|tx| async move {
|
||||
if name.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||
name: ActiveValue::Set(name.to_string()),
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
@@ -95,6 +99,66 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_dev_server_token(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
hashed_token: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
||||
self.transaction(|tx| async move {
|
||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
||||
};
|
||||
if dev_server.user_id != user_id {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
dev_server::Entity::update(dev_server::ActiveModel {
|
||||
hashed_token: ActiveValue::Set(hashed_token.to_string()),
|
||||
..dev_server.clone().into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let dev_server_projects = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok(dev_server_projects)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rename_dev_server(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
name: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
||||
self.transaction(|tx| async move {
|
||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
||||
};
|
||||
if dev_server.user_id != user_id || name.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
dev_server::Entity::update(dev_server::ActiveModel {
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
..dev_server.clone().into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let dev_server_projects = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok(dev_server_projects)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
|
||||
@@ -78,7 +78,6 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
// todo! check user is a project-collaborator
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
return Ok((project.id, room));
|
||||
}
|
||||
@@ -598,6 +597,17 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_dev_server_project(&self, id: DevServerProjectId) -> Result<project::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(project::Entity::find()
|
||||
.filter(project::Column::DevServerProjectId.eq(id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds the given connection to the specified project
|
||||
/// in the current room.
|
||||
pub async fn join_project(
|
||||
|
||||
@@ -138,6 +138,7 @@ pub struct Config {
|
||||
pub zed_client_checksum_seed: Option<String>,
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
pub supermaven_admin_api_key: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use core::fmt::{self, Debug, Formatter};
|
||||
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
|
||||
use sha2::Digest;
|
||||
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
||||
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
@@ -148,7 +149,8 @@ struct Session {
|
||||
peer: Arc<Peer>,
|
||||
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
http_client: IsahcHttpClient,
|
||||
supermaven_client: Option<Arc<SupermavenAdminApi>>,
|
||||
http_client: Arc<IsahcHttpClient>,
|
||||
rate_limiter: Arc<RateLimiter>,
|
||||
_executor: Executor,
|
||||
}
|
||||
@@ -189,6 +191,14 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
match &self.principal {
|
||||
Principal::User(user) => user.admin,
|
||||
Principal::Impersonated { .. } => true,
|
||||
Principal::DevServer(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_server_id(&self) -> Option<DevServerId> {
|
||||
match &self.principal {
|
||||
Principal::User(_) | Principal::Impersonated { .. } => None,
|
||||
@@ -233,6 +243,14 @@ impl UserSession {
|
||||
pub fn user_id(&self) -> UserId {
|
||||
self.0.user_id().unwrap()
|
||||
}
|
||||
|
||||
pub fn email(&self) -> Option<String> {
|
||||
match &self.0.principal {
|
||||
Principal::User(user) => user.email_address.clone(),
|
||||
Principal::Impersonated { user, .. } => user.email_address.clone(),
|
||||
Principal::DevServer(..) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for UserSession {
|
||||
@@ -413,7 +431,10 @@ impl Server {
|
||||
.add_request_handler(user_handler(join_hosted_project))
|
||||
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
||||
.add_request_handler(user_handler(create_dev_server_project))
|
||||
.add_request_handler(user_handler(delete_dev_server_project))
|
||||
.add_request_handler(user_handler(create_dev_server))
|
||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||
.add_request_handler(user_handler(rename_dev_server))
|
||||
.add_request_handler(user_handler(delete_dev_server))
|
||||
.add_request_handler(dev_server_handler(share_dev_server_project))
|
||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||
@@ -560,6 +581,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(get_private_user_info))
|
||||
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
||||
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
||||
.add_request_handler(user_handler(get_supermaven_api_key))
|
||||
.add_streaming_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
move |request, response, session| {
|
||||
@@ -937,13 +959,22 @@ impl Server {
|
||||
tracing::info!("connection opened");
|
||||
|
||||
let http_client = match IsahcHttpClient::new() {
|
||||
Ok(http_client) => http_client,
|
||||
Ok(http_client) => Arc::new(http_client),
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "failed to create HTTP client");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let supermaven_client = if let Some(supermaven_admin_api_key) = this.app_state.config.supermaven_admin_api_key.clone() {
|
||||
Some(Arc::new(SupermavenAdminApi::new(
|
||||
supermaven_admin_api_key.to_string(),
|
||||
http_client.clone(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let session = Session {
|
||||
principal: principal.clone(),
|
||||
connection_id,
|
||||
@@ -954,6 +985,7 @@ impl Server {
|
||||
http_client,
|
||||
rate_limiter: this.app_state.rate_limiter.clone(),
|
||||
_executor: executor.clone(),
|
||||
supermaven_client,
|
||||
};
|
||||
|
||||
if let Err(error) = this.send_initial_client_update(connection_id, &principal, zed_version, send_connection_id, &session).await {
|
||||
@@ -2313,6 +2345,12 @@ async fn create_dev_server(
|
||||
let access_token = auth::random_token();
|
||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||
|
||||
if request.name.is_empty() {
|
||||
return Err(proto::ErrorCode::Forbidden
|
||||
.message("Dev server name cannot be empty".to_string())
|
||||
.anyhow())?;
|
||||
}
|
||||
|
||||
let (dev_server, status) = session
|
||||
.db()
|
||||
.await
|
||||
@@ -2329,6 +2367,71 @@ async fn create_dev_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn regenerate_dev_server_token(
|
||||
request: proto::RegenerateDevServerToken,
|
||||
response: Response<proto::RegenerateDevServerToken>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let access_token = auth::random_token();
|
||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||
|
||||
let connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server_id);
|
||||
if let Some(connection_id) = connection_id {
|
||||
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
|
||||
session
|
||||
.peer
|
||||
.send(connection_id, proto::ShutdownDevServer {})?;
|
||||
let _ = remove_dev_server_connection(dev_server_id, &session).await;
|
||||
}
|
||||
|
||||
let status = session
|
||||
.db()
|
||||
.await
|
||||
.update_dev_server_token(dev_server_id, &hashed_access_token, session.user_id())
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::RegenerateDevServerTokenResponse {
|
||||
dev_server_id: dev_server_id.to_proto(),
|
||||
access_token: auth::generate_dev_server_token(dev_server_id.0 as usize, access_token),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename_dev_server(
|
||||
request: proto::RenameDevServer,
|
||||
response: Response<proto::RenameDevServer>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
if request.name.trim().is_empty() {
|
||||
return Err(proto::ErrorCode::Forbidden
|
||||
.message("Dev server name cannot be empty".to_string())
|
||||
.anyhow())?;
|
||||
}
|
||||
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let dev_server = session.db().await.get_dev_server(dev_server_id).await?;
|
||||
if dev_server.user_id != session.user_id() {
|
||||
return Err(anyhow!(ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let status = session
|
||||
.db()
|
||||
.await
|
||||
.rename_dev_server(dev_server_id, &request.name, session.user_id())
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_dev_server(
|
||||
request: proto::DeleteDevServer,
|
||||
response: Response<proto::DeleteDevServer>,
|
||||
@@ -2349,6 +2452,7 @@ async fn delete_dev_server(
|
||||
session
|
||||
.peer
|
||||
.send(connection_id, proto::ShutdownDevServer {})?;
|
||||
let _ = remove_dev_server_connection(dev_server_id, &session).await;
|
||||
}
|
||||
|
||||
let status = session
|
||||
@@ -2363,6 +2467,68 @@ async fn delete_dev_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_dev_server_project(
|
||||
request: proto::DeleteDevServerProject,
|
||||
response: Response<proto::DeleteDevServerProject>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
|
||||
let dev_server_project = session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server_project(dev_server_project_id)
|
||||
.await?;
|
||||
|
||||
let dev_server = session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server(dev_server_project.dev_server_id)
|
||||
.await?;
|
||||
if dev_server.user_id != session.user_id() {
|
||||
return Err(anyhow!(ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server.id);
|
||||
|
||||
if let Some(dev_server_connection_id) = dev_server_connection_id {
|
||||
let project = session
|
||||
.db()
|
||||
.await
|
||||
.find_dev_server_project(dev_server_project_id)
|
||||
.await;
|
||||
if let Ok(project) = project {
|
||||
unshare_project_internal(
|
||||
project.id,
|
||||
dev_server_connection_id,
|
||||
Some(session.user_id()),
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let (projects, status) = session
|
||||
.db()
|
||||
.await
|
||||
.delete_dev_server_project(dev_server_project_id, dev_server.id, session.user_id())
|
||||
.await?;
|
||||
|
||||
if let Some(dev_server_connection_id) = dev_server_connection_id {
|
||||
session.peer.send(
|
||||
dev_server_connection_id,
|
||||
proto::DevServerInstructions { projects },
|
||||
)?;
|
||||
}
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rejoin_dev_server_projects(
|
||||
request: proto::RejoinRemoteProjects,
|
||||
response: Response<proto::RejoinRemoteProjects>,
|
||||
@@ -2459,7 +2625,8 @@ async fn shutdown_dev_server(
|
||||
session: DevServerSession,
|
||||
) -> Result<()> {
|
||||
response.send(proto::Ack {})?;
|
||||
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await
|
||||
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await?;
|
||||
remove_dev_server_connection(session.dev_server_id(), &session).await
|
||||
}
|
||||
|
||||
async fn shutdown_dev_server_internal(
|
||||
@@ -2499,6 +2666,21 @@ async fn shutdown_dev_server_internal(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dev_server_connection(dev_server_id: DevServerId, session: &Session) -> Result<()> {
|
||||
let dev_server_connection = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server_id);
|
||||
|
||||
if let Some(dev_server_connection) = dev_server_connection {
|
||||
session
|
||||
.connection_pool()
|
||||
.await
|
||||
.remove_connection(dev_server_connection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates other participants with changes to the project
|
||||
async fn update_project(
|
||||
request: proto::UpdateProject,
|
||||
@@ -4147,7 +4329,7 @@ async fn complete_with_open_ai(
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let mut completion_stream = open_ai::stream_completion(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
OPEN_AI_API_URL,
|
||||
&api_key,
|
||||
crate::ai::language_model_request_to_open_ai(request)?,
|
||||
@@ -4211,7 +4393,7 @@ async fn complete_with_google_ai(
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let mut stream = google_ai::stream_generate_content(
|
||||
&session.http_client,
|
||||
session.http_client.clone(),
|
||||
google_ai::API_URL,
|
||||
api_key.as_ref(),
|
||||
crate::ai::language_model_request_to_google_ai(request)?,
|
||||
@@ -4295,7 +4477,7 @@ async fn complete_with_anthropic(
|
||||
.collect();
|
||||
|
||||
let mut stream = anthropic::stream_completion(
|
||||
&session.http_client,
|
||||
session.http_client.clone(),
|
||||
"https://api.anthropic.com",
|
||||
&api_key,
|
||||
anthropic::Request {
|
||||
@@ -4419,7 +4601,7 @@ async fn count_tokens_with_language_model(
|
||||
let api_key = google_ai_api_key
|
||||
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
|
||||
let tokens_response = google_ai::count_tokens(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
google_ai::API_URL,
|
||||
&api_key,
|
||||
crate::ai::count_tokens_request_to_google_ai(request)?,
|
||||
@@ -4438,7 +4620,7 @@ impl RateLimit for ComputeEmbeddingsRateLimit {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
.unwrap_or(5000) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
@@ -4467,7 +4649,7 @@ async fn compute_embeddings(
|
||||
let embeddings = match request.model.as_str() {
|
||||
"openai/text-embedding-3-small" => {
|
||||
open_ai::embed(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
OPEN_AI_API_URL,
|
||||
&api_key,
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
@@ -4510,25 +4692,6 @@ async fn compute_embeddings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct GetCachedEmbeddingsRateLimit;
|
||||
|
||||
impl RateLimit for GetCachedEmbeddingsRateLimit {
|
||||
fn capacity() -> usize {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
chrono::Duration::hours(1)
|
||||
}
|
||||
|
||||
fn db_name() -> &'static str {
|
||||
"get-cached-embeddings"
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_embeddings(
|
||||
request: proto::GetCachedEmbeddings,
|
||||
response: Response<proto::GetCachedEmbeddings>,
|
||||
@@ -4536,11 +4699,6 @@ async fn get_cached_embeddings(
|
||||
) -> Result<()> {
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
session
|
||||
.rate_limiter
|
||||
.check::<GetCachedEmbeddingsRateLimit>(session.user_id())
|
||||
.await?;
|
||||
|
||||
let db = session.db().await;
|
||||
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
|
||||
|
||||
@@ -4563,6 +4721,37 @@ async fn authorize_access_to_language_models(session: &UserSession) -> Result<()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a Supermaven API key for the user
|
||||
async fn get_supermaven_api_key(
|
||||
_request: proto::GetSupermavenApiKey,
|
||||
response: Response<proto::GetSupermavenApiKey>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let user_id: String = session.user_id().to_string();
|
||||
if !session.is_staff() {
|
||||
return Err(anyhow!("supermaven not enabled for this account"))?;
|
||||
}
|
||||
|
||||
let email = session
|
||||
.email()
|
||||
.ok_or_else(|| anyhow!("user must have an email"))?;
|
||||
|
||||
let supermaven_admin_api = session
|
||||
.supermaven_client
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("supermaven not configured"))?;
|
||||
|
||||
let result = supermaven_admin_api
|
||||
.try_get_or_create_user(CreateExternalUserRequest { id: user_id, email })
|
||||
.await?;
|
||||
|
||||
response.send(proto::GetSupermavenApiKeyResponse {
|
||||
api_key: result.api_key,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start receiving chat updates for a channel
|
||||
async fn join_channel_chat(
|
||||
request: proto::JoinChannelChat,
|
||||
|
||||
@@ -263,6 +263,191 @@ async fn test_dev_server_leave_room(
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_delete(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.delete_dev_server_project(store.dev_server_projects().first().unwrap().id, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_server_projects().len(), 0);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_rename(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.rename_dev_server(
|
||||
store.dev_servers().first().unwrap().id,
|
||||
"name-edited".to_string(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers().first().unwrap().name, "name-edited");
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_refresh_access_token(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
cx4: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
// Regenerate the access token
|
||||
let new_token_response = cx1
|
||||
.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.regenerate_dev_server_token(store.dev_servers().first().unwrap().id, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
// Assert that the other client was disconnected
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
|
||||
// Assert that the owner of the dev server does not see the dev server as online anymore
|
||||
let (workspace, cx1) = client1.active_workspace(cx1);
|
||||
cx1.update(|cx| {
|
||||
assert!(workspace.read(cx).project().read(cx).is_disconnected());
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(
|
||||
store.dev_servers().first().unwrap().status,
|
||||
DevServerStatus::Offline
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
// Reconnect the dev server with the new token
|
||||
let _dev_server = server
|
||||
.create_dev_server(new_token_response.access_token, cx4)
|
||||
.await;
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
// Assert that the dev server is online again
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers().len(), 1);
|
||||
assert_eq!(
|
||||
store.dev_servers().first().unwrap().status,
|
||||
DevServerStatus::Online
|
||||
);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_reconnect(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
|
||||
@@ -667,7 +667,7 @@ async fn test_collaborating_with_code_actions(
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
deployed_from_indicator: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -2073,7 +2073,7 @@ struct Row10;"#};
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
editor_cx_b.update_editor(|editor, cx| {
|
||||
@@ -2083,7 +2083,7 @@ struct Row10;"#};
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
@@ -2570,7 +2570,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2591,7 +2594,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2611,7 +2617,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
@@ -2624,7 +2633,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2664,7 +2676,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2685,7 +2700,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2705,7 +2723,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
println!("{:?}", buffer.as_rope().to_string());
|
||||
println!("{:?}", buffer.diff_base());
|
||||
println!(
|
||||
@@ -2727,7 +2748,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -6105,7 +6129,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_k_left(cx: &mut TestAppContext) {
|
||||
async fn test_pane_split_left(cx: &mut TestAppContext) {
|
||||
let (_, client) = TestServer::start1(cx).await;
|
||||
let (workspace, cx) = client.build_test_workspace(cx).await;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
@@ -257,6 +258,11 @@ impl TestServer {
|
||||
})
|
||||
});
|
||||
|
||||
let git_hosting_provider_registry =
|
||||
cx.update(|cx| GitHostingProviderRegistry::default_global(cx));
|
||||
git_hosting_provider_registry
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
@@ -655,6 +661,7 @@ impl TestServer {
|
||||
auto_join_channel_id: None,
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
supermaven_admin_api_key: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ picker.workspace = true
|
||||
project.workspace = true
|
||||
recent_projects.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
release_channel.workspace = true
|
||||
rich_text.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -572,7 +572,7 @@ impl ChatPanel {
|
||||
)
|
||||
.child(
|
||||
self.render_popover_buttons(&cx, message_id, can_delete_message, can_edit_message)
|
||||
.neg_mt_2p5(),
|
||||
.mt_neg_2p5(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1408,6 +1408,11 @@ impl CollabPanel {
|
||||
});
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
self.context_menu.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.update_entries(false, cx);
|
||||
}
|
||||
|
||||
@@ -2149,7 +2154,7 @@ impl CollabPanel {
|
||||
.child(list(self.list_state.clone()).size_full())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(div().mx_2().border_primary(cx).border_t())
|
||||
.child(div().mx_2().border_primary(cx).border_t_1())
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
|
||||
@@ -20,6 +20,7 @@ use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
@@ -96,6 +97,7 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
cx: &AppContext,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = DevicePixels::from(16);
|
||||
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
|
||||
@@ -112,6 +114,8 @@ fn notification_window_options(
|
||||
size: window_size.into(),
|
||||
};
|
||||
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
|
||||
WindowOptions {
|
||||
bounds: Some(bounds),
|
||||
titlebar: None,
|
||||
@@ -122,6 +126,6 @@ fn notification_window_options(
|
||||
display_id: Some(screen.id()),
|
||||
fullscreen: false,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some("dev.zed.Zed".to_owned()),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl RenderOnce for FacePile {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, player)| div().when(ix > 0, |div| div.neg_ml_1()).child(player)),
|
||||
.map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,22 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
};
|
||||
|
||||
for screen in unique_screens {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
if let Some(options) = cx
|
||||
.update(|cx| notification_window_options(screen, window_size, cx))
|
||||
.log_err()
|
||||
{
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,11 +55,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct RespondToCall {
|
||||
accept: bool,
|
||||
}
|
||||
|
||||
struct IncomingCallNotificationState {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
};
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let options = notification_window_options(screen, window_size, cx);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
|
||||
@@ -27,28 +27,39 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
collections.workspace = true
|
||||
client.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
indoc.workspace = true
|
||||
serde_json.workspace = true
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
mod copilot_completion_provider;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
@@ -10,9 +13,9 @@ use gpui::{
|
||||
ModelContext, Task, WeakModel,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
|
||||
LanguageServerName, PointUtf16, ToPointUtf16,
|
||||
language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||
ToPointUtf16,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -32,6 +35,9 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, maybe, paths, ResultExt,
|
||||
};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
[
|
||||
@@ -144,7 +150,6 @@ impl CopilotServer {
|
||||
}
|
||||
|
||||
struct RunningCopilotServer {
|
||||
name: LanguageServerName,
|
||||
lsp: Arc<LanguageServer>,
|
||||
sign_in_status: SignInStatus,
|
||||
registered_buffers: HashMap<EntityId, RegisteredBuffer>,
|
||||
@@ -354,7 +359,9 @@ impl Copilot {
|
||||
let server_id = self.server_id;
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
if all_language_settings(None, cx).copilot_enabled(None, None) {
|
||||
if all_language_settings(None, cx).inline_completions.provider
|
||||
== InlineCompletionProvider::Copilot
|
||||
{
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let start_task = cx
|
||||
.spawn(move |this, cx| {
|
||||
@@ -393,7 +400,6 @@ impl Copilot {
|
||||
http: http.clone(),
|
||||
node_runtime,
|
||||
server: CopilotServer::Running(RunningCopilotServer {
|
||||
name: LanguageServerName(Arc::from("copilot")),
|
||||
lsp: Arc::new(server),
|
||||
sign_in_status: SignInStatus::Authorized,
|
||||
registered_buffers: Default::default(),
|
||||
@@ -467,7 +473,6 @@ impl Copilot {
|
||||
match server {
|
||||
Ok((server, status)) => {
|
||||
this.server = CopilotServer::Running(RunningCopilotServer {
|
||||
name: LanguageServerName(Arc::from("copilot")),
|
||||
lsp: server,
|
||||
sign_in_status: SignInStatus::SignedOut,
|
||||
registered_buffers: Default::default(),
|
||||
@@ -607,9 +612,9 @@ impl Copilot {
|
||||
cx.background_executor().spawn(start_task)
|
||||
}
|
||||
|
||||
pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
|
||||
pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
|
||||
if let CopilotServer::Running(server) = &self.server {
|
||||
Some((&server.name, &server.lsp))
|
||||
Some(&server.lsp)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -943,12 +948,9 @@ impl Copilot {
|
||||
}
|
||||
|
||||
fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
||||
let language_name = language.map(|language| language.name());
|
||||
match language_name.as_deref() {
|
||||
Some("Plain Text") => "plaintext".to_string(),
|
||||
Some(language_name) => language_name.to_lowercase(),
|
||||
None => "plaintext".to_string(),
|
||||
}
|
||||
language
|
||||
.map(|language| language.lsp_id())
|
||||
.unwrap_or_else(|| "plaintext".to_string())
|
||||
}
|
||||
|
||||
fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use copilot::Copilot;
|
||||
use editor::{Direction, InlineCompletionProvider};
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use language::{language_settings::all_language_settings, Buffer, OffsetRangeExt, ToOffset};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
@@ -13,7 +15,7 @@ pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
pub struct CopilotCompletionProvider {
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<copilot::Completion>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Task<Result<()>>,
|
||||
@@ -42,11 +44,11 @@ impl CopilotCompletionProvider {
|
||||
self
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&copilot::Completion> {
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
@@ -71,7 +73,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
let file = buffer.file();
|
||||
let language = buffer.language_at(cursor_position);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings.copilot_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
@@ -196,7 +198,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
if !settings.copilot.feature_enabled {
|
||||
|
||||
let copilot_enabled = settings.inline_completions_enabled(None, None);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,7 +303,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -318,7 +325,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -360,7 +367,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -393,7 +400,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -426,7 +433,7 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
..Default::default()
|
||||
@@ -503,7 +510,7 @@ mod tests {
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
@@ -553,7 +560,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// Setup the editor with a completion request.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -573,7 +582,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -615,7 +624,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -675,7 +684,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one
|
||||
@@ -685,7 +696,7 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
@@ -756,13 +767,13 @@ mod tests {
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_inline_completion_provider(copilot_provider, cx)
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
..Default::default()
|
||||
@@ -788,7 +799,7 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
..Default::default()
|
||||
@@ -829,11 +840,129 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_does_not_prevent_completion_triggers(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one
|
||||
twˇ
|
||||
three
|
||||
"});
|
||||
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
tw|<>
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
two|<>
|
||||
three
|
||||
"},
|
||||
vec!["completion_a_2", "completion_b_2"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
two.|<>
|
||||
three
|
||||
"},
|
||||
vec!["something_else()"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(
|
||||
editor.context_menu_visible(),
|
||||
"On completion trigger input, the completions should be fetched and visible"
|
||||
);
|
||||
assert!(
|
||||
!editor.has_active_inline_completion(cx),
|
||||
"On completion trigger input, copilot suggestion should be dismissed"
|
||||
);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings
|
||||
.copilot
|
||||
.inline_completions
|
||||
.get_or_insert(Default::default())
|
||||
.disabled_globs = Some(vec![".env*".to_string()]);
|
||||
});
|
||||
@@ -888,15 +1017,15 @@ mod tests {
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_inline_completion_provider(copilot_provider, cx)
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.handle_request::<copilot::request::GetCompletions, _, _>(
|
||||
.handle_request::<crate::request::GetCompletions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
completions: vec![copilot::request::Completion {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
@@ -931,21 +1060,21 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<copilot::request::Completion>,
|
||||
completions_cycling: Vec<copilot::request::Completion>,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
) {
|
||||
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
lsp.handle_request::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||||
lsp.handle_request::<crate::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
|
||||
FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
|
||||
@@ -26,7 +26,7 @@ impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
|
||||
impl ModalView for CopilotCodeVerification {}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let status = copilot.read(cx).status();
|
||||
Self {
|
||||
status,
|
||||
@@ -60,7 +60,7 @@ impl CopilotCodeVerification {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.border()
|
||||
.border_1()
|
||||
.border_muted(cx)
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
@@ -1,403 +0,0 @@
|
||||
use crate::sign_in::CopilotCodeVerification;
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
|
||||
Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
struct CopilotErrorToast;
|
||||
|
||||
pub struct CopilotButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Render for CopilotButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return div();
|
||||
}
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => IconName::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
IconName::Copilot
|
||||
} else {
|
||||
IconName::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => IconName::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
return div().child(
|
||||
IconButton::new("copilot-error", icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotErrorToast>(),
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.reinstall(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
);
|
||||
}
|
||||
let this = cx.view().clone();
|
||||
|
||||
div().child(
|
||||
popover_menu("copilot")
|
||||
.menu(move |cx| match status {
|
||||
Status::Authorized => {
|
||||
Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
|
||||
}
|
||||
_ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.trigger(
|
||||
IconButton::new("copilot-icon", icon)
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.entry("Sign In", None, initiate_sign_in).entry(
|
||||
"Disable Copilot",
|
||||
None,
|
||||
move |cx| hide_copilot(fs.clone(), cx),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
None,
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
);
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
None,
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
if let Ok(workspace) = workspace.root_view(cx) {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
None,
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
)
|
||||
.separator()
|
||||
.link(
|
||||
"Copilot Settings",
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
self.editor_enabled = {
|
||||
let file = file.as_ref();
|
||||
Some(
|
||||
file.map(|file| !file.is_private()).unwrap_or(true)
|
||||
&& all_language_settings(file, cx)
|
||||
.copilot_enabled(language, file.map(|file| file.path().as_ref())),
|
||||
)
|
||||
};
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self.editor_subscription = Some((
|
||||
cx.observe(&editor, Self::update_enabled),
|
||||
editor.entity_id().as_u64() as usize,
|
||||
));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakView<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some(!show_copilot_suggestions)
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod copilot_button;
|
||||
mod copilot_completion_provider;
|
||||
mod sign_in;
|
||||
|
||||
pub use copilot_button::*;
|
||||
pub use copilot_completion_provider::*;
|
||||
pub use sign_in::*;
|
||||
@@ -173,6 +173,39 @@ impl Store {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rename_dev_server(
|
||||
&mut self,
|
||||
dev_server_id: DevServerId,
|
||||
name: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::RenameDevServer {
|
||||
dev_server_id: dev_server_id.0,
|
||||
name,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regenerate_dev_server_token(
|
||||
&mut self,
|
||||
dev_server_id: DevServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<proto::RegenerateDevServerTokenResponse>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::RegenerateDevServerToken {
|
||||
dev_server_id: dev_server_id.0,
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_dev_server(
|
||||
&mut self,
|
||||
id: DevServerId,
|
||||
@@ -188,4 +221,20 @@ impl Store {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_dev_server_project(
|
||||
&mut self,
|
||||
id: DevServerProjectId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::DeleteDevServerProject {
|
||||
dev_server_project_id: id.0,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
snippet.workspace = true
|
||||
sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -53,7 +53,13 @@ pub struct SelectToEndOfLine {
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ToggleCodeActions {
|
||||
#[serde(default)]
|
||||
pub deployed_from_indicator: bool,
|
||||
pub deployed_from_indicator: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ToggleTestRunner {
|
||||
#[serde(default)]
|
||||
pub deployed_from_row: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
|
||||
@@ -61,7 +61,7 @@ struct CommitAvatarAsset {
|
||||
impl Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use gpui::{ElementId, HighlightStyle, Hsla};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||
@@ -1066,28 +1065,6 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct HighlightEndpoint {
|
||||
offset: InlayOffset,
|
||||
is_start: bool,
|
||||
tag: Option<TypeId>,
|
||||
style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl PartialOrd for HighlightEndpoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HighlightEndpoint {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.offset
|
||||
.cmp(&other.offset)
|
||||
.then_with(|| other.is_start.cmp(&self.is_start))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct FoldOffset(pub usize);
|
||||
|
||||
|
||||
@@ -34,13 +34,14 @@ mod persistence;
|
||||
mod rust_analyzer_ext;
|
||||
pub mod scroll;
|
||||
mod selections_collection;
|
||||
pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use ::git::permalink::{build_permalink, BuildPermalinkParams};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
pub(crate) use actions::*;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -78,6 +79,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::Runnable;
|
||||
use language::{
|
||||
char_kind,
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
@@ -85,6 +87,7 @@ use language::{
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use task::{ResolvedTask, TaskTemplate};
|
||||
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
@@ -99,7 +102,8 @@ use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::{
|
||||
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
|
||||
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
|
||||
ProjectTransaction, TaskSourceKind, WorktreeId,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
@@ -395,6 +399,19 @@ impl Default for ScrollbarMarkerState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
// We need the column at which the task context evaluation should take place.
|
||||
column: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: text::Point,
|
||||
}
|
||||
|
||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -487,6 +504,8 @@ pub struct Editor {
|
||||
>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: HashMap<u32, RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1167,7 +1186,7 @@ impl CompletionsMenu {
|
||||
|
||||
for mat in &mut matches {
|
||||
let completion = &completions[mat.candidate_id];
|
||||
mat.string = completion.label.text.clone();
|
||||
mat.string.clone_from(&completion.label.text);
|
||||
for position in &mut mat.positions {
|
||||
*position += completion.label.filter_range.start;
|
||||
}
|
||||
@@ -1180,12 +1199,106 @@ impl CompletionsMenu {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CodeActionContents {
|
||||
tasks: Option<Arc<ResolvedTasks>>,
|
||||
actions: Option<Arc<[CodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
fn len(&self) -> usize {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
|
||||
(Some(tasks), None) => tasks.templates.len(),
|
||||
(None, Some(actions)) => actions.len(),
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
|
||||
(Some(tasks), None) => tasks.templates.is_empty(),
|
||||
(None, Some(actions)) => actions.is_empty(),
|
||||
(None, None) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
|
||||
self.tasks
|
||||
.iter()
|
||||
.flat_map(|tasks| {
|
||||
tasks
|
||||
.templates
|
||||
.iter()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
|
||||
})
|
||||
.chain(self.actions.iter().flat_map(|actions| {
|
||||
actions
|
||||
.iter()
|
||||
.map(|action| CodeActionsItem::CodeAction(action.clone()))
|
||||
}))
|
||||
}
|
||||
fn get(&self, index: usize) -> Option<CodeActionsItem> {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => {
|
||||
if index < tasks.templates.len() {
|
||||
tasks
|
||||
.templates
|
||||
.get(index)
|
||||
.cloned()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
|
||||
} else {
|
||||
actions
|
||||
.get(index - tasks.templates.len())
|
||||
.cloned()
|
||||
.map(CodeActionsItem::CodeAction)
|
||||
}
|
||||
}
|
||||
(Some(tasks), None) => tasks
|
||||
.templates
|
||||
.get(index)
|
||||
.cloned()
|
||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
|
||||
(None, Some(actions)) => actions.get(index).cloned().map(CodeActionsItem::CodeAction),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
enum CodeActionsItem {
|
||||
Task(TaskSourceKind, ResolvedTask),
|
||||
CodeAction(CodeAction),
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction(action) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction(action) => action.lsp_action.title.clone(),
|
||||
Self::Task(_, task) => task.resolved_label.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CodeActionsMenu {
|
||||
actions: Arc<[CodeAction]>,
|
||||
actions: CodeActionContents,
|
||||
buffer: Model<Buffer>,
|
||||
selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
deployed_from_indicator: bool,
|
||||
deployed_from_indicator: Option<u32>,
|
||||
}
|
||||
|
||||
impl CodeActionsMenu {
|
||||
@@ -1234,14 +1347,15 @@ impl CodeActionsMenu {
|
||||
) -> (ContextMenuOrigin, AnyElement) {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
|
||||
let element = uniform_list(
|
||||
cx.view().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
move |_this, range, cx| {
|
||||
actions[range.clone()]
|
||||
actions
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
.take(range.end - range.start)
|
||||
.enumerate()
|
||||
.map(|(ix, action)| {
|
||||
let item_ix = range.start + ix;
|
||||
@@ -1260,23 +1374,42 @@ impl CodeActionsMenu {
|
||||
.bg(colors.element_hover)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.whitespace_nowrap()
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(action.lsp_action.title.clone()))
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(action.lsp_action.title.clone()))
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(SharedString::from(task.resolved_label.clone()))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -1291,16 +1424,20 @@ impl CodeActionsMenu {
|
||||
self.actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
|
||||
.max_by_key(|(_, action)| match action {
|
||||
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||
CodeActionsItem::CodeAction(action) => action.lsp_action.title.chars().count(),
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let cursor_position = if self.deployed_from_indicator {
|
||||
ContextMenuOrigin::GutterIndicator(cursor_position.row())
|
||||
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||
ContextMenuOrigin::GutterIndicator(row)
|
||||
} else {
|
||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||
};
|
||||
|
||||
(cursor_position, element)
|
||||
}
|
||||
}
|
||||
@@ -1532,6 +1669,7 @@ impl Editor {
|
||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
tasks: Default::default(),
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1551,8 +1689,9 @@ impl Editor {
|
||||
});
|
||||
}),
|
||||
],
|
||||
tasks_update_task: None,
|
||||
};
|
||||
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
|
||||
this.end_selection(cx);
|
||||
@@ -1757,19 +1896,22 @@ impl Editor {
|
||||
self.completion_provider = Some(hub);
|
||||
}
|
||||
|
||||
pub fn set_inline_completion_provider(
|
||||
pub fn set_inline_completion_provider<T>(
|
||||
&mut self,
|
||||
provider: Model<impl InlineCompletionProvider>,
|
||||
provider: Option<Model<T>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.inline_completion_provider = Some(RegisteredInlineCompletionProvider {
|
||||
_subscription: cx.observe(&provider, |this, _, cx| {
|
||||
if this.focus_handle.is_focused(cx) {
|
||||
this.update_visible_inline_completion(cx);
|
||||
}
|
||||
}),
|
||||
provider: Arc::new(provider),
|
||||
});
|
||||
) where
|
||||
T: InlineCompletionProvider,
|
||||
{
|
||||
self.inline_completion_provider =
|
||||
provider.map(|provider| RegisteredInlineCompletionProvider {
|
||||
_subscription: cx.observe(&provider, |this, _, cx| {
|
||||
if this.focus_handle.is_focused(cx) {
|
||||
this.update_visible_inline_completion(cx);
|
||||
}
|
||||
}),
|
||||
provider: Arc::new(provider),
|
||||
});
|
||||
self.refresh_inline_completion(false, cx);
|
||||
}
|
||||
|
||||
@@ -2676,7 +2818,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
let had_active_copilot_completion = this.has_active_inline_completion(cx);
|
||||
let had_active_inline_completion = this.has_active_inline_completion(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
|
||||
if brace_inserted {
|
||||
@@ -2692,15 +2834,9 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if had_active_copilot_completion {
|
||||
this.refresh_inline_completion(true, cx);
|
||||
if !this.has_active_inline_completion(cx) {
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
}
|
||||
} else {
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
this.refresh_inline_completion(true, cx);
|
||||
}
|
||||
let trigger_in_words = !had_active_inline_completion;
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
||||
this.refresh_inline_completion(true, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2765,7 +2901,7 @@ impl Editor {
|
||||
indent.len = cmp::min(indent.len, start_point.column);
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
let is_cursor = start == end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||
&language_scope
|
||||
@@ -2799,13 +2935,18 @@ impl Editor {
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = language.line_comment_prefixes().filter(|_| {
|
||||
let is_comment_extension_enabled =
|
||||
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
|
||||
is_cursor && is_comment_extension_enabled
|
||||
});
|
||||
let get_comment_delimiter = |delimiters: &[Arc<str>]| {
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.settings_at(0, cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let delimiters = language.line_comment_prefixes();
|
||||
let max_len_of_delimiter =
|
||||
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) =
|
||||
@@ -2834,12 +2975,7 @@ impl Editor {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let comment_delimiter = if let Some(delimiters) = comment_delimiter {
|
||||
get_comment_delimiter(delimiters)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
} else {
|
||||
(None, false)
|
||||
@@ -3053,7 +3189,12 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
fn trigger_completion_on_input(
|
||||
&mut self,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !EditorSettings::get_global(cx).show_completions_on_input {
|
||||
return;
|
||||
}
|
||||
@@ -3062,7 +3203,7 @@ impl Editor {
|
||||
if self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.is_completion_trigger(selection.head(), text, cx)
|
||||
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
|
||||
{
|
||||
self.show_completions(&ShowCompletions, cx);
|
||||
} else {
|
||||
@@ -3685,38 +3826,130 @@ impl Editor {
|
||||
|
||||
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
|
||||
let mut context_menu = self.context_menu.write();
|
||||
if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
|
||||
*context_menu = None;
|
||||
cx.notify();
|
||||
return;
|
||||
if let Some(ContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
|
||||
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
|
||||
// Toggle if we're selecting the same one
|
||||
*context_menu = None;
|
||||
cx.notify();
|
||||
return;
|
||||
} else {
|
||||
// Otherwise, clear it and start a new one
|
||||
*context_menu = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
drop(context_menu);
|
||||
|
||||
let deployed_from_indicator = action.deployed_from_indicator;
|
||||
let mut task = self.code_actions_task.take();
|
||||
let action = action.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some(prev_task) = task {
|
||||
prev_task.await;
|
||||
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let spawned_test_task = this.update(&mut cx, |this, cx| {
|
||||
if this.focus_handle.is_focused(cx) {
|
||||
if let Some((buffer, actions)) = this.available_code_actions.clone() {
|
||||
this.completion_tasks.clear();
|
||||
this.discard_inline_completion(cx);
|
||||
*this.context_menu.write() =
|
||||
Some(ContextMenu::CodeActions(CodeActionsMenu {
|
||||
buffer,
|
||||
actions,
|
||||
selected_item: Default::default(),
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
deployed_from_indicator,
|
||||
}));
|
||||
cx.notify();
|
||||
let snapshot = this.snapshot(cx);
|
||||
let display_row = action.deployed_from_indicator.unwrap_or_else(|| {
|
||||
this.selections
|
||||
.newest::<Point>(cx)
|
||||
.head()
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row()
|
||||
});
|
||||
|
||||
let buffer_point =
|
||||
DisplayPoint::new(display_row, 0).to_point(&snapshot.display_snapshot);
|
||||
let buffer_row = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(buffer_point.row)
|
||||
.map(|(_, Range { start, .. })| start);
|
||||
let tasks = this.tasks.get(&display_row).map(|t| Arc::new(t.to_owned()));
|
||||
let (buffer, code_actions) = this.available_code_actions.clone().unzip();
|
||||
if tasks.is_none() && code_actions.is_none() {
|
||||
return None;
|
||||
}
|
||||
let buffer = buffer.or_else(|| {
|
||||
let snapshot = this.snapshot(cx);
|
||||
let (buffer_snapshot, _) =
|
||||
snapshot.buffer_snapshot.buffer_line_for_row(display_row)?;
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
this.buffer().read(cx).buffer(buffer_id)
|
||||
});
|
||||
let Some(buffer) = buffer else {
|
||||
return None;
|
||||
};
|
||||
this.completion_tasks.clear();
|
||||
this.discard_inline_completion(cx);
|
||||
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||
|(tasks, (workspace, _))| {
|
||||
if let Some(buffer_point) = buffer_row {
|
||||
let position = Point::new(buffer_point.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,
|
||||
};
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks::task_context_for_location(workspace, location, cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
let tasks = tasks
|
||||
.zip(task_context.as_ref())
|
||||
.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(),
|
||||
position: Point::new(display_row, tasks.column),
|
||||
})
|
||||
});
|
||||
let spawn_straight_away = tasks
|
||||
.as_ref()
|
||||
.map_or(false, |tasks| tasks.templates.len() == 1)
|
||||
&& code_actions
|
||||
.as_ref()
|
||||
.map_or(true, |actions| actions.is_empty());
|
||||
*this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu {
|
||||
buffer,
|
||||
actions: CodeActionContents {
|
||||
tasks,
|
||||
actions: code_actions,
|
||||
},
|
||||
selected_item: Default::default(),
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
deployed_from_indicator,
|
||||
}));
|
||||
if spawn_straight_away {
|
||||
if let Some(task) =
|
||||
this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx)
|
||||
{
|
||||
cx.notify();
|
||||
return Some(task);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
Some(Task::ready(Ok(())))
|
||||
})?;
|
||||
if let Some(task) = spawned_test_task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
@@ -3734,23 +3967,47 @@ impl Editor {
|
||||
return None;
|
||||
};
|
||||
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
|
||||
let action = actions_menu.actions.get(action_ix)?.clone();
|
||||
let title = action.lsp_action.title.clone();
|
||||
let action = actions_menu.actions.get(action_ix)?;
|
||||
let title = action.label();
|
||||
let buffer = actions_menu.buffer;
|
||||
let workspace = self.workspace()?;
|
||||
|
||||
let apply_code_actions = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.clone()
|
||||
.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer, action, true, cx)
|
||||
});
|
||||
let workspace = workspace.downgrade();
|
||||
Some(cx.spawn(|editor, cx| async move {
|
||||
let project_transaction = apply_code_actions.await?;
|
||||
Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
|
||||
}))
|
||||
match action {
|
||||
CodeActionsItem::Task(task_source_kind, resolved_task) => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace::tasks::schedule_resolved_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
resolved_task,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
CodeActionsItem::CodeAction(action) => {
|
||||
let apply_code_actions = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.clone()
|
||||
.update(cx, |project, cx| {
|
||||
project.apply_code_action(buffer, action, true, cx)
|
||||
});
|
||||
let workspace = workspace.downgrade();
|
||||
Some(cx.spawn(|editor, cx| async move {
|
||||
let project_transaction = apply_code_actions.await?;
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace,
|
||||
project_transaction,
|
||||
title,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_project_transaction(
|
||||
@@ -4005,7 +4262,7 @@ impl Editor {
|
||||
if !self.show_inline_completions
|
||||
|| !provider.is_enabled(&buffer, cursor_buffer_position, cx)
|
||||
{
|
||||
self.clear_inline_completion(cx);
|
||||
self.discard_inline_completion(cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -4207,20 +4464,14 @@ impl Editor {
|
||||
self.discard_inline_completion(cx);
|
||||
}
|
||||
|
||||
fn clear_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(old_completion) = self.active_inline_completion.take() {
|
||||
self.splice_inlays(vec![old_completion.id], Vec::new(), cx);
|
||||
}
|
||||
self.discard_inline_completion(cx);
|
||||
}
|
||||
|
||||
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
|
||||
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
||||
}
|
||||
|
||||
pub fn render_code_actions_indicator(
|
||||
fn render_code_actions_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
row: u32,
|
||||
is_active: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<IconButton> {
|
||||
@@ -4231,10 +4482,10 @@ impl Editor {
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(|editor, _e, cx| {
|
||||
.on_click(cx.listener(move |editor, _e, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
deployed_from_indicator: Some(row),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -4245,6 +4496,39 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_tasks(&mut self) {
|
||||
self.tasks.clear()
|
||||
}
|
||||
|
||||
fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
|
||||
if let Some(_) = self.tasks.insert(row, tasks) {
|
||||
// This case should hopefully be rare, but just in case...
|
||||
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
|
||||
}
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
is_active: bool,
|
||||
row: u32,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> IconButton {
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Play)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(move |editor, _e, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: Some(row),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_fold_indicators(
|
||||
&mut self,
|
||||
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||
@@ -4977,10 +5261,16 @@ impl Editor {
|
||||
if !revert_changes.is_empty() {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||
for (buffer_id, buffer_revert_ranges) in revert_changes {
|
||||
for (buffer_id, changes) in revert_changes {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(buffer_revert_ranges, None, cx);
|
||||
buffer.edit(
|
||||
changes.into_iter().map(|(range, text)| {
|
||||
(range, text.to_string().map(Arc::<str>::from))
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5013,7 +5303,7 @@ impl Editor {
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>> {
|
||||
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>> {
|
||||
let mut revert_changes = HashMap::default();
|
||||
self.buffer.update(cx, |multi_buffer, cx| {
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
@@ -5025,14 +5315,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn prepare_revert_change(
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>>,
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
multi_buffer: &MultiBuffer,
|
||||
hunk: &DiffHunk<u32>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = buffer.diff_base()?.get(hunk.diff_base_byte_range.clone())?;
|
||||
let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone());
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
|
||||
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
|
||||
@@ -5041,9 +5331,8 @@ impl Editor {
|
||||
.start
|
||||
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
|
||||
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
|
||||
.then(probe.1.as_ref().cmp(original_text))
|
||||
}) {
|
||||
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), Arc::from(original_text)));
|
||||
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
@@ -7180,10 +7469,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
// If the language has line comments, toggle those.
|
||||
if let Some(full_comment_prefixes) = language
|
||||
.line_comment_prefixes()
|
||||
.filter(|prefixes| !prefixes.is_empty())
|
||||
{
|
||||
let full_comment_prefixes = language.line_comment_prefixes();
|
||||
if !full_comment_prefixes.is_empty() {
|
||||
let first_prefix = full_comment_prefixes
|
||||
.first()
|
||||
.expect("prefixes is non-empty");
|
||||
@@ -7402,6 +7689,127 @@ impl Editor {
|
||||
self.select_larger_syntax_node_stack = stack;
|
||||
}
|
||||
|
||||
fn refresh_runnables(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
let project = self.project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Ok(display_snapshot) = this.update(&mut cx, |this, cx| {
|
||||
this.display_map.update(cx, |map, cx| map.snapshot(cx))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(project) = project else {
|
||||
return;
|
||||
};
|
||||
if project
|
||||
.update(&mut cx, |this, _| this.is_remote())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
// Do not display any test indicators in remote projects.
|
||||
return;
|
||||
}
|
||||
let new_rows =
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let snapshot = display_snapshot.clone();
|
||||
async move {
|
||||
Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let rows = Self::refresh_runnable_display_rows(
|
||||
project,
|
||||
display_snapshot,
|
||||
new_rows,
|
||||
cx.clone(),
|
||||
);
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.clear_tasks();
|
||||
for (row, tasks) in rows {
|
||||
this.insert_tasks(row, tasks);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
fn fetch_runnable_ranges(
|
||||
snapshot: &DisplaySnapshot,
|
||||
range: Range<Anchor>,
|
||||
) -> Vec<(Range<usize>, Runnable)> {
|
||||
snapshot.buffer_snapshot.runnable_ranges(range).collect()
|
||||
}
|
||||
fn refresh_runnable_display_rows(
|
||||
project: Model<Project>,
|
||||
snapshot: DisplaySnapshot,
|
||||
runnable_ranges: Vec<(Range<usize>, Runnable)>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Vec<(u32, RunnableTasks)> {
|
||||
runnable_ranges
|
||||
.into_iter()
|
||||
.filter_map(|(multi_buffer_range, mut runnable)| {
|
||||
let (tasks, _) = cx
|
||||
.update(|cx| Self::resolve_runnable(project.clone(), &mut runnable, cx))
|
||||
.ok()?;
|
||||
if tasks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let point = multi_buffer_range.start.to_display_point(&snapshot);
|
||||
Some((
|
||||
point.row(),
|
||||
RunnableTasks {
|
||||
templates: tasks,
|
||||
column: point.column(),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_runnable(
|
||||
project: Model<Project>,
|
||||
runnable: &mut Runnable,
|
||||
cx: &WindowContext<'_>,
|
||||
) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) {
|
||||
let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
|
||||
let worktree_id = project
|
||||
.buffer_for_id(runnable.buffer)
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map(|file| WorktreeId::from_usize(file.worktree_id()));
|
||||
|
||||
(project.task_inventory().clone(), worktree_id)
|
||||
});
|
||||
|
||||
let inventory = inventory.read(cx);
|
||||
let tags = mem::take(&mut runnable.tags);
|
||||
let mut tags: Vec<_> = tags
|
||||
.into_iter()
|
||||
.flat_map(|tag| {
|
||||
let tag = tag.0.clone();
|
||||
inventory
|
||||
.list_tasks(Some(runnable.language.clone()), worktree_id)
|
||||
.into_iter()
|
||||
.filter(move |(_, template)| {
|
||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||
})
|
||||
})
|
||||
.sorted_by_key(|(kind, _)| kind.to_owned())
|
||||
.collect();
|
||||
if let Some((leading_tag_source, _)) = tags.first() {
|
||||
// Strongest source wins; if we have worktree tag binding, prefer that to
|
||||
// global and language bindings;
|
||||
// if we have a global binding, prefer that to language binding.
|
||||
let first_mismatch = tags
|
||||
.iter()
|
||||
.position(|(tag_source, _)| tag_source != leading_tag_source);
|
||||
if let Some(index) = first_mismatch {
|
||||
tags.truncate(index);
|
||||
}
|
||||
}
|
||||
|
||||
(tags, worktree_id)
|
||||
}
|
||||
|
||||
pub fn move_to_enclosing_bracket(
|
||||
&mut self,
|
||||
_: &MoveToEnclosingBracket,
|
||||
@@ -9182,17 +9590,23 @@ impl Editor {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let selection = selections.iter().peekable().next();
|
||||
|
||||
build_permalink(BuildPermalinkParams {
|
||||
remote_url: &origin_url,
|
||||
sha: &sha,
|
||||
path: &path,
|
||||
selection: selection.map(|selection| {
|
||||
let range = selection.range();
|
||||
let start = range.start.row;
|
||||
let end = range.end.row;
|
||||
start..end
|
||||
}),
|
||||
})
|
||||
let (provider, remote) =
|
||||
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
|
||||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
Ok(provider.build_permalink(
|
||||
remote,
|
||||
BuildPermalinkParams {
|
||||
sha: &sha,
|
||||
path: &path,
|
||||
selection: selection.map(|selection| {
|
||||
let range = selection.range();
|
||||
let start = range.start.row;
|
||||
let end = range.end.row;
|
||||
start..end
|
||||
}),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {
|
||||
@@ -9698,7 +10112,11 @@ impl Editor {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
|
||||
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
|
||||
multi_buffer::Event::Reparsed => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(cx));
|
||||
|
||||
cx.emit(EditorEvent::Reparsed);
|
||||
}
|
||||
multi_buffer::Event::LanguageChanged => {
|
||||
cx.emit(EditorEvent::Reparsed);
|
||||
cx.notify();
|
||||
@@ -9942,12 +10360,14 @@ impl Editor {
|
||||
.raw_user_settings()
|
||||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true));
|
||||
let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
|
||||
|
||||
let copilot_enabled = all_language_settings(file, cx).inline_completions.provider
|
||||
== language::language_settings::InlineCompletionProvider::Copilot;
|
||||
let copilot_enabled_for_language = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.show_copilot_suggestions;
|
||||
.show_inline_completions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_editor_event(
|
||||
@@ -10837,34 +11257,12 @@ impl ViewInputHandler for Editor {
|
||||
}
|
||||
|
||||
trait SelectionExt {
|
||||
fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
|
||||
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
|
||||
fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
|
||||
fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
|
||||
-> Range<u32>;
|
||||
}
|
||||
|
||||
impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
|
||||
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
|
||||
let start = self.start.to_point(buffer);
|
||||
let end = self.end.to_point(buffer);
|
||||
if self.reversed {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize> {
|
||||
let start = self.start.to_offset(buffer);
|
||||
let end = self.end.to_offset(buffer);
|
||||
if self.reversed {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint> {
|
||||
let start = self
|
||||
.start
|
||||
|
||||
@@ -9026,7 +9026,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
|
||||
.collect::<String>(),
|
||||
cx,
|
||||
);
|
||||
buffer.set_diff_base(Some(sample_text), cx);
|
||||
buffer.set_diff_base(Some(sample_text.into()), cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
@@ -10041,17 +10041,17 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
"vvvv\nwwww\nxxxx\nyyyy\nzzzz\n@@@@\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
|
||||
let buffer_1 = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(modified_sample_text_1.to_string(), cx);
|
||||
buffer.set_diff_base(Some(sample_text_1.clone()), cx);
|
||||
buffer.set_diff_base(Some(sample_text_1.clone().into()), cx);
|
||||
buffer
|
||||
});
|
||||
let buffer_2 = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(modified_sample_text_2.to_string(), cx);
|
||||
buffer.set_diff_base(Some(sample_text_2.clone()), cx);
|
||||
buffer.set_diff_base(Some(sample_text_2.clone().into()), cx);
|
||||
buffer
|
||||
});
|
||||
let buffer_3 = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(modified_sample_text_3.to_string(), cx);
|
||||
buffer.set_diff_base(Some(sample_text_3.clone()), cx);
|
||||
buffer.set_diff_base(Some(sample_text_3.clone().into()), cx);
|
||||
buffer
|
||||
});
|
||||
|
||||
@@ -11351,7 +11351,7 @@ fn assert_hunk_revert(
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -12,10 +12,11 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
|
||||
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
|
||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
|
||||
GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
|
||||
OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::ParticipantIndex;
|
||||
@@ -427,10 +428,12 @@ impl EditorElement {
|
||||
let mut click_count = event.click_count;
|
||||
let mut modifiers = event.modifiers;
|
||||
|
||||
if gutter_hitbox.is_hovered(cx) {
|
||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||
} else if let Some(hovered_hunk) = hovered_hunk {
|
||||
if let Some(hovered_hunk) = hovered_hunk {
|
||||
editor.expand_diff_hunk(None, hovered_hunk, cx);
|
||||
cx.notify();
|
||||
return;
|
||||
} else if gutter_hitbox.is_hovered(cx) {
|
||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||
} else if !text_hitbox.is_hovered(cx) {
|
||||
return;
|
||||
}
|
||||
@@ -635,7 +638,7 @@ impl EditorElement {
|
||||
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
|
||||
|
||||
if let Some(point) = point_for_position.as_valid() {
|
||||
hover_at(editor, Some(point), cx);
|
||||
hover_at(editor, Some((point, &position_map.snapshot)), cx);
|
||||
Self::update_visible_cursor(editor, point, position_map, cx);
|
||||
} else {
|
||||
hover_at(editor, None, cx);
|
||||
@@ -1372,6 +1375,56 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let active_task_indicator_row =
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
actions,
|
||||
..
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
{
|
||||
actions
|
||||
.tasks
|
||||
.as_ref()
|
||||
.map(|tasks| tasks.position.row)
|
||||
.or_else(|| *deployed_from_indicator)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
editor
|
||||
.tasks
|
||||
.keys()
|
||||
.map(|row| {
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(*row) == active_task_indicator_row,
|
||||
*row,
|
||||
cx,
|
||||
);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
*row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
button
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -1383,35 +1436,28 @@ impl EditorElement {
|
||||
) -> Option<AnyElement> {
|
||||
let mut active = false;
|
||||
let mut button = None;
|
||||
let row = newest_selection_head.row();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
active = matches!(
|
||||
editor.context_menu.read().as_ref(),
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
button = editor.render_code_actions_indicator(&self.style, active, cx);
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
..
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
{
|
||||
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
|
||||
};
|
||||
button = editor.render_code_actions_indicator(&self.style, row, active, cx);
|
||||
});
|
||||
|
||||
let mut button = button?.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
let button = prepaint_gutter_button(
|
||||
button?,
|
||||
row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = blame_width;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
||||
Some(button)
|
||||
}
|
||||
|
||||
@@ -1767,7 +1813,7 @@ impl EditorElement {
|
||||
.pr(gpui::px(8.))
|
||||
.rounded_md()
|
||||
.shadow_md()
|
||||
.border()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.justify_between()
|
||||
@@ -2349,6 +2395,10 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for test_indicators in layout.test_indicators.iter_mut() {
|
||||
test_indicators.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
|
||||
indicator.paint(cx);
|
||||
}
|
||||
@@ -3222,6 +3272,39 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: u32,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> AnyElement {
|
||||
let mut button = button.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = blame_width;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = row as f32 * line_height - scroll_pixel_position.y;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
||||
button
|
||||
}
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
blame: &gpui::Model<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
@@ -3937,18 +4020,33 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
if gutter_settings.code_actions {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
let has_test_indicator = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.tasks
|
||||
.contains_key(&newest_selection_head.row());
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_indicators = self.layout_run_indicators(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !context_menu_visible && !cx.has_active_drag() {
|
||||
self.layout_hover_popovers(
|
||||
&snapshot,
|
||||
@@ -4049,6 +4147,7 @@ impl Element for EditorElement {
|
||||
visible_cursors,
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
fold_indicators,
|
||||
tab_invisible,
|
||||
@@ -4168,6 +4267,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
max_row: u32,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
fold_indicators: Vec<Option<AnyElement>>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
|
||||
@@ -145,7 +145,8 @@ mod tests {
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
.unindent()
|
||||
.into(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
@@ -181,7 +182,8 @@ mod tests {
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
.unindent()
|
||||
.into(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -4,10 +4,7 @@ use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git::{
|
||||
blame::{Blame, BlameEntry},
|
||||
hosting_provider::HostingProvider,
|
||||
permalink::{build_commit_permalink, parse_git_remote_url},
|
||||
pull_request::{extract_pull_request, PullRequest},
|
||||
Oid,
|
||||
parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest,
|
||||
};
|
||||
use gpui::{Model, ModelContext, Subscription, Task};
|
||||
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
|
||||
@@ -50,13 +47,23 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct GitRemote {
|
||||
pub host: HostingProvider,
|
||||
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GitRemote {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GitRemote")
|
||||
.field("host", &self.host.name())
|
||||
.field("owner", &self.owner)
|
||||
.field("repo", &self.repo)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRemote {
|
||||
pub fn host_supports_avatars(&self) -> bool {
|
||||
self.host.supports_avatars()
|
||||
@@ -323,6 +330,7 @@ impl GitBlame {
|
||||
let snapshot = self.buffer.read(cx).snapshot();
|
||||
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
let provider_registry = GitHostingProviderRegistry::default_global(cx);
|
||||
|
||||
self.task = cx.spawn(|this, mut cx| async move {
|
||||
let result = cx
|
||||
@@ -338,9 +346,14 @@ impl GitBlame {
|
||||
} = blame.await?;
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, remote_url, &permalinks, &languages)
|
||||
.await;
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
&permalinks,
|
||||
provider_registry,
|
||||
&languages,
|
||||
)
|
||||
.await;
|
||||
|
||||
anyhow::Ok((entries, commit_details))
|
||||
}
|
||||
@@ -431,19 +444,22 @@ async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
) -> HashMap<Oid, CommitDetails> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for (oid, message) in messages {
|
||||
let parsed_message = parse_markdown(&message, &languages).await;
|
||||
|
||||
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
|
||||
Some(build_commit_permalink(
|
||||
git::permalink::BuildCommitPermalinkParams {
|
||||
remote: git_remote,
|
||||
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
|
||||
Some(provider.build_commit_permalink(
|
||||
git_remote,
|
||||
git::BuildCommitPermalinkParams {
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
@@ -455,15 +471,17 @@ async fn parse_commit_messages(
|
||||
deprecated_permalinks.get(&oid).cloned()
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url.as_ref().map(|remote| GitRemote {
|
||||
host: remote.provider.clone(),
|
||||
owner: remote.owner.to_string(),
|
||||
repo: remote.repo.to_string(),
|
||||
});
|
||||
let remote = parsed_remote_url
|
||||
.as_ref()
|
||||
.map(|(provider, remote)| GitRemote {
|
||||
host: provider.clone(),
|
||||
owner: remote.owner.to_string(),
|
||||
repo: remote.repo.to_string(),
|
||||
});
|
||||
|
||||
let pull_request = parsed_remote_url
|
||||
.as_ref()
|
||||
.and_then(|remote| extract_pull_request(remote, &message));
|
||||
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
|
||||
@@ -732,7 +732,7 @@ mod tests {
|
||||
|
||||
cx.cx
|
||||
.cx
|
||||
.simulate_mouse_move(screen_coord.unwrap(), Modifiers::command_shift());
|
||||
.simulate_mouse_move(screen_coord.unwrap(), None, Modifiers::command_shift());
|
||||
|
||||
requests.next().await;
|
||||
cx.run_until_parked();
|
||||
@@ -802,7 +802,7 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -828,7 +828,7 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -847,7 +847,7 @@ mod tests {
|
||||
// No definitions returned
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
|
||||
});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
@@ -863,7 +863,7 @@ mod tests {
|
||||
fn test() { do_work(); }
|
||||
fn do_work() { teˇst(); }
|
||||
"});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::none());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
|
||||
|
||||
// Assert no link highlights
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -907,7 +907,7 @@ mod tests {
|
||||
fn do_work() { test(); }
|
||||
"});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
fn test() { do_work(); }
|
||||
@@ -919,7 +919,7 @@ mod tests {
|
||||
fn test() { do_work(); }
|
||||
fn do_work() { tesˇt(); }
|
||||
"});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
fn test() { do_work(); }
|
||||
@@ -1009,7 +1009,7 @@ mod tests {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(requests.try_next().is_err());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -1123,7 +1123,7 @@ mod tests {
|
||||
});
|
||||
// Press cmd to trigger highlight
|
||||
let hover_point = cx.pixel_position_for(midpoint);
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
@@ -1142,7 +1142,7 @@ mod tests {
|
||||
assert_set_eq!(actual_highlights, vec![&expected_highlight]);
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::none());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
|
||||
// Assert no link highlights
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
@@ -1186,7 +1186,7 @@ mod tests {
|
||||
Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
|
||||
"});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
|
||||
"});
|
||||
@@ -1214,7 +1214,7 @@ mod tests {
|
||||
let screen_coord =
|
||||
cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(
|
||||
indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
|
||||
);
|
||||
@@ -1239,7 +1239,7 @@ mod tests {
|
||||
let screen_coord =
|
||||
cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(
|
||||
indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
|
||||
);
|
||||
|
||||
@@ -31,15 +31,20 @@ pub const HOVER_POPOVER_GAP: Pixels = px(10.);
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
let head = editor.selections.newest_display(cx).head();
|
||||
show_hover(editor, head, true, cx);
|
||||
let snapshot = editor.snapshot(cx);
|
||||
show_hover(editor, head, &snapshot, true, cx);
|
||||
}
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
|
||||
pub fn hover_at(
|
||||
editor: &mut Editor,
|
||||
point: Option<(DisplayPoint, &EditorSnapshot)>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if EditorSettings::get_global(cx).hover_popover_enabled {
|
||||
if let Some(point) = point {
|
||||
show_hover(editor, point, false, cx);
|
||||
if let Some((point, snapshot)) = point {
|
||||
show_hover(editor, point, snapshot, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
@@ -160,6 +165,7 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
fn show_hover(
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
snapshot: &EditorSnapshot,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
@@ -167,7 +173,6 @@ fn show_hover(
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
|
||||
|
||||
let (buffer, buffer_position) = if let Some(output) = editor
|
||||
@@ -234,6 +239,7 @@ fn show_hover(
|
||||
return;
|
||||
}
|
||||
}
|
||||
let snapshot = snapshot.clone();
|
||||
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
@@ -659,7 +665,10 @@ mod tests {
|
||||
fn test() { printˇln!(); }
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
hover_at(editor, Some((hover_point, &snapshot)), cx)
|
||||
});
|
||||
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
|
||||
|
||||
// After delay, hover should be visible.
|
||||
@@ -705,7 +714,10 @@ mod tests {
|
||||
let mut request = cx
|
||||
.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
hover_at(editor, Some((hover_point, &snapshot)), cx)
|
||||
});
|
||||
cx.background_executor
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
request.next().await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
@@ -14,7 +14,8 @@ use util::{debug_panic, RangeExt};
|
||||
use crate::{
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight,
|
||||
Editor, ExpandAllHunkDiffs, RangeToAnchorExt, ToDisplayPoint, ToggleHunkDiff,
|
||||
Editor, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint,
|
||||
ToggleHunkDiff,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -215,34 +216,29 @@ impl Editor {
|
||||
let hunk_end = hunk.multi_buffer_range.end;
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let (diff_base_buffer, deleted_text_range, deleted_text_lines) =
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let hunk = buffer_diff_hunk(&snapshot, multi_buffer_row_range.clone())?;
|
||||
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
|
||||
if buffer_ranges.len() == 1 {
|
||||
let (buffer, _, _) = buffer_ranges.pop()?;
|
||||
let diff_base_buffer = diff_base_buffer
|
||||
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
|
||||
.or_else(|| create_diff_base_buffer(&buffer, cx));
|
||||
let buffer = buffer.read(cx);
|
||||
let deleted_text_lines = buffer.diff_base().and_then(|diff_base| {
|
||||
Some(
|
||||
diff_base
|
||||
.get(hunk.diff_base_byte_range.clone())?
|
||||
.lines()
|
||||
.count(),
|
||||
)
|
||||
});
|
||||
Some((
|
||||
diff_base_buffer?,
|
||||
hunk.diff_base_byte_range,
|
||||
deleted_text_lines,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let hunk = buffer_diff_hunk(&snapshot, multi_buffer_row_range.clone())?;
|
||||
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
|
||||
if buffer_ranges.len() == 1 {
|
||||
let (buffer, _, _) = buffer_ranges.pop()?;
|
||||
let diff_base_buffer = diff_base_buffer
|
||||
.or_else(|| self.current_diff_base_buffer(&buffer, cx))
|
||||
.or_else(|| create_diff_base_buffer(&buffer, cx))?;
|
||||
let buffer = buffer.read(cx);
|
||||
let deleted_text_lines = buffer.diff_base().map(|diff_base| {
|
||||
let diff_start_row = diff_base
|
||||
.offset_to_point(hunk.diff_base_byte_range.start)
|
||||
.row;
|
||||
let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row;
|
||||
let line_count = diff_end_row - diff_start_row;
|
||||
line_count as u8
|
||||
})?;
|
||||
Some((diff_base_buffer, deleted_text_lines))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| {
|
||||
probe
|
||||
@@ -255,13 +251,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let block = match hunk.status {
|
||||
DiffHunkStatus::Removed => self.add_deleted_lines(
|
||||
deleted_text_lines,
|
||||
hunk_start,
|
||||
diff_base_buffer,
|
||||
deleted_text_range,
|
||||
cx,
|
||||
),
|
||||
DiffHunkStatus::Removed => {
|
||||
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
|
||||
}
|
||||
DiffHunkStatus::Added => {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
hunk_start..hunk_end,
|
||||
@@ -276,13 +268,7 @@ impl Editor {
|
||||
Some(added_hunk_color(cx)),
|
||||
cx,
|
||||
);
|
||||
self.add_deleted_lines(
|
||||
deleted_text_lines,
|
||||
hunk_start,
|
||||
diff_base_buffer,
|
||||
deleted_text_range,
|
||||
cx,
|
||||
)
|
||||
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
|
||||
}
|
||||
};
|
||||
self.expanded_hunks.hunks.insert(
|
||||
@@ -299,43 +285,20 @@ impl Editor {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn add_deleted_lines(
|
||||
&mut self,
|
||||
deleted_text_lines: Option<usize>,
|
||||
hunk_start: Anchor,
|
||||
diff_base_buffer: Model<Buffer>,
|
||||
deleted_text_range: Range<usize>,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Option<BlockId> {
|
||||
if let Some(deleted_text_lines) = deleted_text_lines {
|
||||
self.insert_deleted_text_block(
|
||||
hunk_start,
|
||||
diff_base_buffer,
|
||||
deleted_text_range,
|
||||
deleted_text_lines as u8,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
debug_panic!("Found no deleted text for removed hunk on position {hunk_start:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_deleted_text_block(
|
||||
&mut self,
|
||||
position: Anchor,
|
||||
diff_base_buffer: Model<Buffer>,
|
||||
deleted_text_range: Range<usize>,
|
||||
deleted_text_height: u8,
|
||||
hunk: &HunkToExpand,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Option<BlockId> {
|
||||
let deleted_hunk_color = deleted_hunk_color(cx);
|
||||
let (editor_height, editor_with_deleted_text) =
|
||||
editor_with_deleted_text(diff_base_buffer, deleted_text_range, deleted_hunk_color, cx);
|
||||
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
|
||||
let parent_gutter_offset = self.gutter_dimensions.width + self.gutter_dimensions.margin;
|
||||
let mut new_block_ids = self.insert_blocks(
|
||||
Some(BlockProperties {
|
||||
position,
|
||||
position: hunk.multi_buffer_range.start,
|
||||
height: editor_height.max(deleted_text_height),
|
||||
style: BlockStyle::Flex,
|
||||
render: Box::new(move |_| {
|
||||
@@ -542,12 +505,12 @@ fn create_diff_base_buffer(buffer: &Model<Buffer>, cx: &mut AppContext) -> Optio
|
||||
buffer
|
||||
.update(cx, |buffer, _| {
|
||||
let language = buffer.language().cloned();
|
||||
let diff_base = buffer.diff_base().map(|s| s.to_owned());
|
||||
Some((diff_base?, language))
|
||||
let diff_base = buffer.diff_base()?.clone();
|
||||
Some((buffer.line_ending(), diff_base, language))
|
||||
})
|
||||
.map(|(diff_base, language)| {
|
||||
.map(|(line_ending, diff_base, language)| {
|
||||
cx.new_model(|cx| {
|
||||
let buffer = Buffer::local(diff_base, cx);
|
||||
let buffer = Buffer::local_normalized(diff_base, line_ending, cx);
|
||||
match language {
|
||||
Some(language) => buffer.with_language(language, cx),
|
||||
None => buffer,
|
||||
@@ -570,10 +533,11 @@ fn deleted_hunk_color(cx: &AppContext) -> Hsla {
|
||||
|
||||
fn editor_with_deleted_text(
|
||||
diff_base_buffer: Model<Buffer>,
|
||||
deleted_text_range: Range<usize>,
|
||||
deleted_color: Hsla,
|
||||
hunk: &HunkToExpand,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> (u8, View<Editor>) {
|
||||
let parent_editor = cx.view().downgrade();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let multi_buffer =
|
||||
cx.new_model(|_| MultiBuffer::without_headers(0, language::Capability::ReadOnly));
|
||||
@@ -581,7 +545,7 @@ fn editor_with_deleted_text(
|
||||
multi_buffer.push_excerpts(
|
||||
diff_base_buffer,
|
||||
Some(ExcerptRange {
|
||||
context: deleted_text_range,
|
||||
context: hunk.diff_base_byte_range.clone(),
|
||||
primary: None,
|
||||
}),
|
||||
cx,
|
||||
@@ -602,6 +566,40 @@ fn editor_with_deleted_text(
|
||||
.anchor_after(editor.buffer.read(cx).len(cx));
|
||||
|
||||
editor.highlight_rows::<DiffRowHighlight>(start..end, Some(deleted_color), cx);
|
||||
let hunk_related_subscription = cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.try_cancel();
|
||||
});
|
||||
});
|
||||
editor._subscriptions.push(hunk_related_subscription);
|
||||
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
|
||||
let diff_base_range = hunk.diff_base_byte_range.clone();
|
||||
editor.register_action::<RevertSelectedHunks>(move |_, cx| {
|
||||
parent_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) = editor.buffer().update(cx, |buffer, cx| {
|
||||
let (_, buffer, _) =
|
||||
buffer.excerpt_containing(original_multi_buffer_range.start, cx)?;
|
||||
let original_text =
|
||||
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
|
||||
Some((buffer, Arc::from(original_text.to_string())))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
Some((
|
||||
original_multi_buffer_range.start.text_anchor
|
||||
..original_multi_buffer_range.end.text_anchor,
|
||||
original_text,
|
||||
)),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||
);
|
||||
fn accept(&mut self, cx: &mut ModelContext<Self>);
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>);
|
||||
fn active_completion_text(
|
||||
&self,
|
||||
fn active_completion_text<'a>(
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &AppContext,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&str>;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub trait InlineCompletionProviderHandle {
|
||||
fn accept(&self, cx: &mut AppContext);
|
||||
fn discard(&self, cx: &mut AppContext);
|
||||
fn active_completion_text<'a>(
|
||||
&self,
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &'a AppContext,
|
||||
@@ -110,7 +110,7 @@ where
|
||||
}
|
||||
|
||||
fn active_completion_text<'a>(
|
||||
&self,
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &'a AppContext,
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn deploy_context_menu(
|
||||
.action(
|
||||
"Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
deployed_from_indicator: None,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
|
||||
@@ -69,7 +69,7 @@ impl SelectionsCollection {
|
||||
self.next_selection_id = other.next_selection_id;
|
||||
self.line_mode = other.line_mode;
|
||||
self.disjoint = other.disjoint.clone();
|
||||
self.pending = other.pending.clone();
|
||||
self.pending.clone_from(&other.pending);
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
|
||||
118
crates/editor/src/tasks.rs
Normal file
118
crates/editor/src/tasks.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::Editor;
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use gpui::WindowContext;
|
||||
use language::{BasicContextProvider, ContextProvider};
|
||||
use project::{Location, WorktreeId};
|
||||
use task::{TaskContext, TaskVariables};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) fn task_context_for_location(
|
||||
workspace: &Workspace,
|
||||
location: Location,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Option<TaskContext> {
|
||||
let cwd = workspace::tasks::task_cwd(workspace, cx)
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
let buffer = location.buffer.clone();
|
||||
let language_context_provider = buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.and_then(|language| language.context_provider())
|
||||
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
||||
|
||||
let worktree_abs_path = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.map(|file| WorktreeId::from_usize(file.worktree_id()))
|
||||
.and_then(|worktree_id| {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
});
|
||||
let task_variables = combine_task_variables(
|
||||
worktree_abs_path.as_deref(),
|
||||
location,
|
||||
language_context_provider.as_ref(),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
Some(TaskContext {
|
||||
cwd,
|
||||
task_variables,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn task_context_with_editor(
|
||||
workspace: &Workspace,
|
||||
editor: &mut Editor,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Option<TaskContext> {
|
||||
let (selection, buffer, editor_snapshot) = {
|
||||
let selection = editor.selections.newest::<usize>(cx);
|
||||
let (buffer, _, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_offset(selection.start, cx)?;
|
||||
let snapshot = editor.snapshot(cx);
|
||||
Some((selection, buffer, snapshot))
|
||||
}?;
|
||||
let selection_range = selection.range();
|
||||
let start = editor_snapshot
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(selection_range.start)
|
||||
.text_anchor;
|
||||
let end = editor_snapshot
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(selection_range.end)
|
||||
.text_anchor;
|
||||
let location = Location {
|
||||
buffer,
|
||||
range: start..end,
|
||||
};
|
||||
task_context_for_location(workspace, location, cx)
|
||||
}
|
||||
|
||||
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
||||
let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
else {
|
||||
return Default::default();
|
||||
};
|
||||
editor.update(cx, |editor, cx| {
|
||||
task_context_with_editor(workspace, editor, cx).unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
fn combine_task_variables(
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: Location,
|
||||
context_provider: &dyn ContextProvider,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> anyhow::Result<TaskVariables> {
|
||||
if context_provider.is_basic() {
|
||||
context_provider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building basic provider context")
|
||||
} else {
|
||||
let mut basic_context = BasicContextProvider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building basic default context")?;
|
||||
basic_context.extend(
|
||||
context_provider
|
||||
.build_context(worktree_abs_path, &location, cx)
|
||||
.context("building provider context ")?,
|
||||
);
|
||||
Ok(basic_context)
|
||||
}
|
||||
}
|
||||
@@ -99,12 +99,13 @@ pub fn editor_hunks(
|
||||
.read(cx)
|
||||
.excerpt_containing(Point::new(hunk.associated_range.start, 0), cx)
|
||||
.expect("no excerpt for expanded buffer's hunk start");
|
||||
let diff_base = &buffer
|
||||
let diff_base = buffer
|
||||
.read(cx)
|
||||
.diff_base()
|
||||
.expect("should have a diff base for expanded hunk")
|
||||
[hunk.diff_base_byte_range.clone()];
|
||||
(diff_base.to_owned(), hunk.status(), display_range)
|
||||
.slice(hunk.diff_base_byte_range.clone())
|
||||
.to_string();
|
||||
(diff_base, hunk.status(), display_range)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -134,16 +135,13 @@ pub fn expanded_hunks(
|
||||
.read(cx)
|
||||
.excerpt_containing(expanded_hunk.hunk_range.start, cx)
|
||||
.expect("no excerpt for expanded buffer's hunk start");
|
||||
let diff_base = &buffer
|
||||
let diff_base = buffer
|
||||
.read(cx)
|
||||
.diff_base()
|
||||
.expect("should have a diff base for expanded hunk")
|
||||
[expanded_hunk.diff_base_byte_range.clone()];
|
||||
(
|
||||
diff_base.to_owned(),
|
||||
expanded_hunk.status,
|
||||
hunk_display_range,
|
||||
)
|
||||
.slice(expanded_hunk.diff_base_byte_range.clone())
|
||||
.to_string();
|
||||
(diff_base, expanded_hunk.status, hunk_display_range)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use text::Rope;
|
||||
use ui::Context;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
@@ -271,7 +272,7 @@ impl EditorTestContext {
|
||||
}
|
||||
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
|
||||
let diff_base = diff_base.map(String::from);
|
||||
let diff_base = diff_base.map(Rope::from);
|
||||
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
|
||||
}
|
||||
|
||||
|
||||
@@ -852,7 +852,7 @@ impl Render for ExtensionsPage {
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.border_b()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
|
||||
@@ -454,7 +454,7 @@ impl Render for FeedbackModal {
|
||||
.flex_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.feedback_editor.clone()),
|
||||
@@ -466,7 +466,7 @@ impl Render for FeedbackModal {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border()
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
|
||||
@@ -20,7 +20,7 @@ impl SystemSpecs {
|
||||
pub fn new(cx: &AppContext) -> Self {
|
||||
let app_version = AppVersion::global(cx).to_string();
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let os_name = cx.app_metadata().os_name;
|
||||
let os_name = cx.app_metadata().os.name;
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::new().with_memory(MemoryRefreshKind::everything()),
|
||||
);
|
||||
@@ -28,7 +28,8 @@ impl SystemSpecs {
|
||||
let architecture = env::consts::ARCH;
|
||||
let os_version = cx
|
||||
.app_metadata()
|
||||
.os_version
|
||||
.os
|
||||
.version
|
||||
.map(|os_version| os_version.to_string());
|
||||
let commit_sha = match release_channel {
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use git::GitHostingProviderRegistry;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
@@ -117,12 +118,19 @@ pub struct Metadata {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
|
||||
Self { git_binary_path }
|
||||
pub fn new(
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
git_hosting_provider_registry,
|
||||
git_binary_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +482,7 @@ impl Fs for RealFs {
|
||||
Arc::new(Mutex::new(RealGitRepository::new(
|
||||
libgit_repository,
|
||||
self.git_binary_path.clone(),
|
||||
self.git_hosting_provider_registry.clone(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,21 +13,23 @@ path = "src/git.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
git2.workspace = true
|
||||
gpui.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
smol.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
serde.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
parking_lot.workspace = true
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::commit::get_messages;
|
||||
use crate::permalink::{build_commit_permalink, parse_git_remote_url, BuildCommitPermalinkParams};
|
||||
use crate::Oid;
|
||||
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::Path};
|
||||
use text::Rope;
|
||||
use time;
|
||||
@@ -34,6 +34,7 @@ impl Blame {
|
||||
path: &Path,
|
||||
content: &Rope,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> Result<Self> {
|
||||
let output = run_git_blame(git_binary, working_directory, path, &content)?;
|
||||
let mut entries = parse_git_blame(&output)?;
|
||||
@@ -41,18 +42,22 @@ impl Blame {
|
||||
|
||||
let mut permalinks = HashMap::default();
|
||||
let mut unique_shas = HashSet::default();
|
||||
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for entry in entries.iter_mut() {
|
||||
unique_shas.insert(entry.sha);
|
||||
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
|
||||
// now do the parsing.
|
||||
if let Some(remote) = parsed_remote_url.as_ref() {
|
||||
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
|
||||
permalinks.entry(entry.sha).or_insert_with(|| {
|
||||
build_commit_permalink(BuildCommitPermalinkParams {
|
||||
provider.build_commit_permalink(
|
||||
remote,
|
||||
sha: entry.sha.to_string().as_str(),
|
||||
})
|
||||
BuildCommitPermalinkParams {
|
||||
sha: entry.sha.to_string().as_str(),
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -255,15 +260,21 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
|
||||
.get(&new_entry.sha)
|
||||
.and_then(|slot| entries.get(*slot))
|
||||
{
|
||||
new_entry.author = existing_entry.author.clone();
|
||||
new_entry.author_mail = existing_entry.author_mail.clone();
|
||||
new_entry.author.clone_from(&existing_entry.author);
|
||||
new_entry
|
||||
.author_mail
|
||||
.clone_from(&existing_entry.author_mail);
|
||||
new_entry.author_time = existing_entry.author_time;
|
||||
new_entry.author_tz = existing_entry.author_tz.clone();
|
||||
new_entry.committer = existing_entry.committer.clone();
|
||||
new_entry.committer_mail = existing_entry.committer_mail.clone();
|
||||
new_entry.author_tz.clone_from(&existing_entry.author_tz);
|
||||
new_entry.committer.clone_from(&existing_entry.committer);
|
||||
new_entry
|
||||
.committer_mail
|
||||
.clone_from(&existing_entry.committer_mail);
|
||||
new_entry.committer_time = existing_entry.committer_time;
|
||||
new_entry.committer_tz = existing_entry.committer_tz.clone();
|
||||
new_entry.summary = existing_entry.summary.clone();
|
||||
new_entry
|
||||
.committer_tz
|
||||
.clone_from(&existing_entry.committer_tz);
|
||||
new_entry.summary.clone_from(&existing_entry.summary);
|
||||
}
|
||||
|
||||
current_entry.replace(new_entry);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use rope::Rope;
|
||||
use std::{iter, ops::Range};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point};
|
||||
@@ -179,11 +180,12 @@ impl BufferDiff {
|
||||
self.tree = SumTree::new();
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
|
||||
pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
let mut tree = SumTree::new();
|
||||
|
||||
let diff_base_text = diff_base.to_string();
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
let patch = Self::diff(&diff_base_text, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
@@ -345,6 +347,7 @@ mod tests {
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
let diff_base_rope = Rope::from(diff_base.clone());
|
||||
|
||||
let buffer_text = "
|
||||
one
|
||||
@@ -355,7 +358,7 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||
let mut diff = BufferDiff::new();
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
smol::block_on(diff.update(&diff_base_rope, &buffer));
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
@@ -364,7 +367,7 @@ mod tests {
|
||||
);
|
||||
|
||||
buffer.edit([(0..0, "point five\n")]);
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
smol::block_on(diff.update(&diff_base_rope, &buffer));
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
@@ -391,6 +394,7 @@ mod tests {
|
||||
ten
|
||||
"
|
||||
.unindent();
|
||||
let diff_base_rope = Rope::from(diff_base.clone());
|
||||
|
||||
let buffer_text = "
|
||||
A
|
||||
@@ -415,7 +419,7 @@ mod tests {
|
||||
|
||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||
let mut diff = BufferDiff::new();
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
smol::block_on(diff.update(&diff_base_rope, &buffer));
|
||||
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||
|
||||
assert_hunks(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod hosting_provider;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
@@ -7,12 +9,11 @@ use std::str::FromStr;
|
||||
pub use git2 as libgit;
|
||||
pub use lazy_static::lazy_static;
|
||||
|
||||
pub use crate::hosting_provider::*;
|
||||
|
||||
pub mod blame;
|
||||
pub mod commit;
|
||||
pub mod diff;
|
||||
pub mod hosting_provider;
|
||||
pub mod permalink;
|
||||
pub mod pull_request;
|
||||
pub mod repository;
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -1,110 +1,177 @@
|
||||
use core::fmt;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::BTreeMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{AppContext, Global};
|
||||
use parking_lot::RwLock;
|
||||
use url::Url;
|
||||
use util::{codeberg, github, http::HttpClient};
|
||||
use util::http::HttpClient;
|
||||
|
||||
use crate::Oid;
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub enum HostingProvider {
|
||||
Github,
|
||||
Gitlab,
|
||||
Gitee,
|
||||
Bitbucket,
|
||||
Sourcehut,
|
||||
Codeberg,
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct PullRequest {
|
||||
pub number: u32,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl HostingProvider {
|
||||
pub(crate) fn base_url(&self) -> Url {
|
||||
let base_url = match self {
|
||||
Self::Github => "https://github.com",
|
||||
Self::Gitlab => "https://gitlab.com",
|
||||
Self::Gitee => "https://gitee.com",
|
||||
Self::Bitbucket => "https://bitbucket.org",
|
||||
Self::Sourcehut => "https://git.sr.ht",
|
||||
Self::Codeberg => "https://codeberg.org",
|
||||
};
|
||||
pub struct BuildCommitPermalinkParams<'a> {
|
||||
pub sha: &'a str,
|
||||
}
|
||||
|
||||
Url::parse(&base_url).unwrap()
|
||||
}
|
||||
pub struct BuildPermalinkParams<'a> {
|
||||
pub sha: &'a str,
|
||||
pub path: &'a str,
|
||||
pub selection: Option<Range<u32>>,
|
||||
}
|
||||
|
||||
/// Returns the fragment portion of the URL for the selected lines in
|
||||
/// the representation the [`GitHostingProvider`] expects.
|
||||
pub(crate) fn line_fragment(&self, selection: &Range<u32>) -> String {
|
||||
/// A Git hosting provider.
|
||||
#[async_trait]
|
||||
pub trait GitHostingProvider {
|
||||
/// Returns the name of the provider.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the base URL of the provider.
|
||||
fn base_url(&self) -> Url;
|
||||
|
||||
/// Returns a permalink to a Git commit on this hosting provider.
|
||||
fn build_commit_permalink(
|
||||
&self,
|
||||
remote: &ParsedGitRemote,
|
||||
params: BuildCommitPermalinkParams,
|
||||
) -> Url;
|
||||
|
||||
/// Returns a permalink to a file and/or selection on this hosting provider.
|
||||
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
|
||||
|
||||
/// Returns whether this provider supports avatars.
|
||||
fn supports_avatars(&self) -> bool;
|
||||
|
||||
/// Returns a URL fragment to the given line selection.
|
||||
fn line_fragment(&self, selection: &Range<u32>) -> String {
|
||||
if selection.start == selection.end {
|
||||
let line = selection.start + 1;
|
||||
|
||||
match self {
|
||||
Self::Github | Self::Gitlab | Self::Gitee | Self::Sourcehut | Self::Codeberg => {
|
||||
format!("L{}", line)
|
||||
}
|
||||
Self::Bitbucket => format!("lines-{}", line),
|
||||
}
|
||||
self.format_line_number(line)
|
||||
} else {
|
||||
let start_line = selection.start + 1;
|
||||
let end_line = selection.end + 1;
|
||||
|
||||
match self {
|
||||
Self::Github | Self::Codeberg => format!("L{}-L{}", start_line, end_line),
|
||||
Self::Gitlab | Self::Gitee | Self::Sourcehut => {
|
||||
format!("L{}-{}", start_line, end_line)
|
||||
}
|
||||
Self::Bitbucket => format!("lines-{}:{}", start_line, end_line),
|
||||
}
|
||||
self.format_line_numbers(start_line, end_line)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_avatars(&self) -> bool {
|
||||
match self {
|
||||
HostingProvider::Github | HostingProvider::Codeberg => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// Returns a formatted line number to be placed in a permalink URL.
|
||||
fn format_line_number(&self, line: u32) -> String;
|
||||
|
||||
pub async fn commit_author_avatar_url(
|
||||
/// Returns a formatted range of line numbers to be placed in a permalink URL.
|
||||
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
|
||||
|
||||
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
|
||||
|
||||
fn extract_pull_request(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
repo: &str,
|
||||
commit: Oid,
|
||||
client: Arc<dyn HttpClient>,
|
||||
_remote: &ParsedGitRemote,
|
||||
_message: &str,
|
||||
) -> Option<PullRequest> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn commit_author_avatar_url(
|
||||
&self,
|
||||
_repo_owner: &str,
|
||||
_repo: &str,
|
||||
_commit: Oid,
|
||||
_http_client: Arc<dyn HttpClient>,
|
||||
) -> Result<Option<Url>> {
|
||||
Ok(match self {
|
||||
HostingProvider::Github => {
|
||||
let commit = commit.to_string();
|
||||
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
|
||||
.await?
|
||||
.map(|author| -> Result<Url, url::ParseError> {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
url.set_query(Some("size=128"));
|
||||
Ok(url)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
HostingProvider::Codeberg => {
|
||||
let commit = commit.to_string();
|
||||
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
|
||||
.await?
|
||||
.map(|author| Url::parse(&author.avatar_url))
|
||||
.transpose()
|
||||
}
|
||||
_ => Ok(None),
|
||||
}?)
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HostingProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let name = match self {
|
||||
HostingProvider::Github => "GitHub",
|
||||
HostingProvider::Gitlab => "GitLab",
|
||||
HostingProvider::Gitee => "Gitee",
|
||||
HostingProvider::Bitbucket => "Bitbucket",
|
||||
HostingProvider::Sourcehut => "Sourcehut",
|
||||
HostingProvider::Codeberg => "Codeberg",
|
||||
};
|
||||
write!(f, "{}", name)
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct GlobalGitHostingProviderRegistry(Arc<GitHostingProviderRegistry>);
|
||||
|
||||
impl Global for GlobalGitHostingProviderRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GitHostingProviderRegistryState {
|
||||
providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GitHostingProviderRegistry {
|
||||
state: RwLock<GitHostingProviderRegistryState>,
|
||||
}
|
||||
|
||||
impl GitHostingProviderRegistry {
|
||||
/// Returns the global [`GitHostingProviderRegistry`].
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
|
||||
}
|
||||
|
||||
/// Returns the global [`GitHostingProviderRegistry`].
|
||||
///
|
||||
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
|
||||
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
||||
cx.default_global::<GlobalGitHostingProviderRegistry>()
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Sets the global [`GitHostingProviderRegistry`].
|
||||
pub fn set_global(registry: Arc<GitHostingProviderRegistry>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalGitHostingProviderRegistry(registry));
|
||||
}
|
||||
|
||||
/// Returns a new [`GitHostingProviderRegistry`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(GitHostingProviderRegistryState {
|
||||
providers: BTreeMap::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of all [`GitHostingProvider`]s in the registry.
|
||||
pub fn list_hosting_providers(
|
||||
&self,
|
||||
) -> Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
|
||||
self.state.read().providers.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Adds the provided [`GitHostingProvider`] to the registry.
|
||||
pub fn register_hosting_provider(
|
||||
&self,
|
||||
provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||
) {
|
||||
self.state
|
||||
.write()
|
||||
.providers
|
||||
.insert(provider.name(), provider);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedGitRemote<'a> {
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
||||
pub fn parse_git_remote_url(
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
url: &str,
|
||||
) -> Option<(
|
||||
Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||
ParsedGitRemote,
|
||||
)> {
|
||||
provider_registry
|
||||
.list_hosting_providers()
|
||||
.into_iter()
|
||||
.find_map(|provider| {
|
||||
provider
|
||||
.parse_remote_url(&url)
|
||||
.map(|parsed_remote| (provider, parsed_remote))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,680 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use url::Url;
|
||||
|
||||
use crate::hosting_provider::HostingProvider;
|
||||
|
||||
pub struct BuildPermalinkParams<'a> {
|
||||
pub remote_url: &'a str,
|
||||
pub sha: &'a str,
|
||||
pub path: &'a str,
|
||||
pub selection: Option<Range<u32>>,
|
||||
}
|
||||
|
||||
pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
|
||||
let BuildPermalinkParams {
|
||||
remote_url,
|
||||
sha,
|
||||
path,
|
||||
selection,
|
||||
} = params;
|
||||
|
||||
let ParsedGitRemote {
|
||||
provider,
|
||||
owner,
|
||||
repo,
|
||||
} = parse_git_remote_url(remote_url)
|
||||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
let path = match provider {
|
||||
HostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
|
||||
HostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
|
||||
HostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
|
||||
HostingProvider::Bitbucket => format!("{owner}/{repo}/src/{sha}/{path}"),
|
||||
HostingProvider::Sourcehut => format!("~{owner}/{repo}/tree/{sha}/item/{path}"),
|
||||
HostingProvider::Codeberg => format!("{owner}/{repo}/src/commit/{sha}/{path}"),
|
||||
};
|
||||
let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
|
||||
|
||||
let mut permalink = provider.base_url().join(&path).unwrap();
|
||||
permalink.set_fragment(line_fragment.as_deref());
|
||||
Ok(permalink)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedGitRemote<'a> {
|
||||
pub provider: HostingProvider,
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
||||
pub struct BuildCommitPermalinkParams<'a> {
|
||||
pub remote: &'a ParsedGitRemote<'a>,
|
||||
pub sha: &'a str,
|
||||
}
|
||||
|
||||
pub fn build_commit_permalink(params: BuildCommitPermalinkParams) -> Url {
|
||||
let BuildCommitPermalinkParams { sha, remote } = params;
|
||||
|
||||
let ParsedGitRemote {
|
||||
provider,
|
||||
owner,
|
||||
repo,
|
||||
} = remote;
|
||||
|
||||
let path = match provider {
|
||||
HostingProvider::Github => format!("{owner}/{repo}/commit/{sha}"),
|
||||
HostingProvider::Gitlab => format!("{owner}/{repo}/-/commit/{sha}"),
|
||||
HostingProvider::Gitee => format!("{owner}/{repo}/commit/{sha}"),
|
||||
HostingProvider::Bitbucket => format!("{owner}/{repo}/commits/{sha}"),
|
||||
HostingProvider::Sourcehut => format!("~{owner}/{repo}/commit/{sha}"),
|
||||
HostingProvider::Codeberg => format!("{owner}/{repo}/commit/{sha}"),
|
||||
};
|
||||
|
||||
provider.base_url().join(&path).unwrap()
|
||||
}
|
||||
|
||||
pub fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
||||
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@github.com:")
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Github,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@gitlab.com:")
|
||||
.trim_start_matches("https://gitlab.com/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Gitlab,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@gitee.com:")
|
||||
.trim_start_matches("https://gitee.com/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Gitee,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.contains("bitbucket.org") {
|
||||
let (_, repo_with_owner) = url.trim_end_matches(".git").split_once("bitbucket.org")?;
|
||||
let (owner, repo) = repo_with_owner
|
||||
.trim_start_matches('/')
|
||||
.trim_start_matches(':')
|
||||
.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Bitbucket,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.starts_with("git@git.sr.ht:") || url.starts_with("https://git.sr.ht/") {
|
||||
// sourcehut indicates a repo with '.git' suffix as a separate repo.
|
||||
// For example, "git@git.sr.ht:~username/repo" and "git@git.sr.ht:~username/repo.git"
|
||||
// are two distinct repositories.
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@git.sr.ht:~")
|
||||
.trim_start_matches("https://git.sr.ht/~");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Sourcehut,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.starts_with("git@codeberg.org:") || url.starts_with("https://codeberg.org/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@codeberg.org:")
|
||||
.trim_start_matches("https://codeberg.org/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once('/')?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: HostingProvider::Codeberg,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitee.com:libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitee.com:libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitee.com:libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitee.com/libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitee.com/libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitee_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitee.com/libkitten/zed.git",
|
||||
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_remote_url_bitbucket_https_with_username() {
|
||||
let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git";
|
||||
let parsed = parse_git_remote_url(url).unwrap();
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_remote_url_bitbucket_https_without_username() {
|
||||
let url = "https://bitbucket.org/thorstenzed/testingrepo.git";
|
||||
let parsed = parse_git_remote_url(url).unwrap();
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_remote_url_bitbucket_git() {
|
||||
let url = "git@bitbucket.org:thorstenzed/testingrepo.git";
|
||||
let parsed = parse_git_remote_url(url).unwrap();
|
||||
assert!(matches!(parsed.provider, HostingProvider::Bitbucket));
|
||||
assert_eq!(parsed.owner, "thorstenzed");
|
||||
assert_eq!(parsed.repo, "testingrepo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
|
||||
sha: "f00b4r",
|
||||
path: "main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
|
||||
sha: "f00b4r",
|
||||
path: "main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url =
|
||||
"https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_bitbucket_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@bitbucket.org:thorstenzed/testingrepo.git",
|
||||
sha: "f00b4r",
|
||||
path: "main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url =
|
||||
"https://bitbucket.org/thorstenzed/testingrepo/src/f00b4r/main.rs#lines-24:48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_ssh_url_with_git_prefix() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@git.sr.ht:~rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@git.sr.ht:~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_sourcehut_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://git.sr.ht/~rajveermalviya/zed",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://git.sr.ht/~rajveermalviya/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/zed/src/main.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@codeberg.org:rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(6..6),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://codeberg.org/rajveermalviya/zed.git",
|
||||
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(23..47),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://codeberg.org/rajveermalviya/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/zed/src/main.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use lazy_static::lazy_static;
|
||||
use url::Url;
|
||||
|
||||
use crate::{hosting_provider::HostingProvider, permalink::ParsedGitRemote};
|
||||
|
||||
lazy_static! {
|
||||
static ref GITHUB_PULL_REQUEST_NUMBER: regex::Regex =
|
||||
regex::Regex::new(r"\(#(\d+)\)$").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PullRequest {
|
||||
pub number: u32,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
pub fn extract_pull_request(remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
|
||||
match remote.provider {
|
||||
HostingProvider::Github => {
|
||||
let line = message.lines().next()?;
|
||||
let capture = GITHUB_PULL_REQUEST_NUMBER.captures(line)?;
|
||||
let number = capture.get(1)?.as_str().parse::<u32>().ok()?;
|
||||
|
||||
let mut url = remote.provider.base_url();
|
||||
let path = format!("/{}/{}/pull/{}", remote.owner, remote.repo, number);
|
||||
url.set_path(&path);
|
||||
|
||||
Some(PullRequest { number, url })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unindent::Unindent;
|
||||
|
||||
use crate::{
|
||||
hosting_provider::HostingProvider, permalink::ParsedGitRemote,
|
||||
pull_request::extract_pull_request,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_github_pull_requests() {
|
||||
let remote = ParsedGitRemote {
|
||||
provider: HostingProvider::Github,
|
||||
owner: "zed-industries",
|
||||
repo: "zed",
|
||||
};
|
||||
|
||||
let message = "This does not contain a pull request";
|
||||
assert!(extract_pull_request(&remote, message).is_none());
|
||||
|
||||
// Pull request number at end of first line
|
||||
let message = r#"
|
||||
project panel: do not expand collapsed worktrees on "collapse all entries" (#10687)
|
||||
|
||||
Fixes #10597
|
||||
|
||||
Release Notes:
|
||||
|
||||
- Fixed "project panel: collapse all entries" expanding collapsed worktrees.
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
extract_pull_request(&remote, &message)
|
||||
.unwrap()
|
||||
.url
|
||||
.as_str(),
|
||||
"https://github.com/zed-industries/zed/pull/10687"
|
||||
);
|
||||
|
||||
// Pull request number in middle of line, which we want to ignore
|
||||
let message = r#"
|
||||
Follow-up to #10687 to fix problems
|
||||
|
||||
See the original PR, this is a fix.
|
||||
"#
|
||||
.unindent();
|
||||
assert!(extract_pull_request(&remote, &message).is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::blame::Blame;
|
||||
use crate::GitHostingProviderRegistry;
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashMap;
|
||||
use git2::{BranchType, StatusShow};
|
||||
@@ -71,13 +72,19 @@ impl std::fmt::Debug for dyn GitRepository {
|
||||
pub struct RealGitRepository {
|
||||
pub repository: LibGitRepository,
|
||||
pub git_binary_path: PathBuf,
|
||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
}
|
||||
|
||||
impl RealGitRepository {
|
||||
pub fn new(repository: LibGitRepository, git_binary_path: Option<PathBuf>) -> Self {
|
||||
pub fn new(
|
||||
repository: LibGitRepository,
|
||||
git_binary_path: Option<PathBuf>,
|
||||
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
|
||||
hosting_provider_registry,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +253,7 @@ impl GitRepository for RealGitRepository {
|
||||
path,
|
||||
&content,
|
||||
remote_url,
|
||||
self.hosting_provider_registry.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
30
crates/git_hosting_providers/Cargo.toml
Normal file
30
crates/git_hosting_providers/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "git_hosting_providers"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/git_hosting_providers.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
isahc.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
unindent.workspace = true
|
||||
serde_json.workspace = true
|
||||
pretty_assertions.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