Compare commits
210 Commits
ex-local
...
register-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c563ec303 | ||
|
|
050b95e429 | ||
|
|
1385c49e59 | ||
|
|
8a87a7f459 | ||
|
|
fc3de7f7f1 | ||
|
|
0482017a95 | ||
|
|
6abc2c2d36 | ||
|
|
69bfcb0ed7 | ||
|
|
38544aef26 | ||
|
|
ea713e95fe | ||
|
|
6505bdbb3f | ||
|
|
0dd773f591 | ||
|
|
a638946007 | ||
|
|
d94ada12d0 | ||
|
|
387c428720 | ||
|
|
9e858de69b | ||
|
|
cb695ce71c | ||
|
|
39cf37a154 | ||
|
|
be37bd11a7 | ||
|
|
aa8ee02367 | ||
|
|
005bfdbb96 | ||
|
|
253f7ac4a7 | ||
|
|
0808b636cf | ||
|
|
73f42f0146 | ||
|
|
b4a2dd4c8e | ||
|
|
3373bad23b | ||
|
|
0186e9bd42 | ||
|
|
14b337b2db | ||
|
|
ee04ad637f | ||
|
|
347825cc09 | ||
|
|
3f20635ff0 | ||
|
|
f2a4e081cc | ||
|
|
ca616d8824 | ||
|
|
09920413a6 | ||
|
|
3e5621833b | ||
|
|
86f7066a1b | ||
|
|
8768d46f2c | ||
|
|
fe7a1aa921 | ||
|
|
dbc37ba469 | ||
|
|
b2fe8a7afb | ||
|
|
23b862c978 | ||
|
|
3012bb9293 | ||
|
|
0ed4a12ba7 | ||
|
|
f9a85025ea | ||
|
|
7abca1ccbc | ||
|
|
03a05d14ea | ||
|
|
27719b4aad | ||
|
|
d6d7da567a | ||
|
|
3071e07205 | ||
|
|
acbe8aebfc | ||
|
|
46b5a6f763 | ||
|
|
c4c2a0a027 | ||
|
|
6976208e21 | ||
|
|
9d23e5733c | ||
|
|
3a6e91abcb | ||
|
|
6055b45ee1 | ||
|
|
88f90c12ed | ||
|
|
0d74f982a5 | ||
|
|
ca90b8555d | ||
|
|
8516d81e13 | ||
|
|
af589ff25f | ||
|
|
d2bbfbb3bf | ||
|
|
413f4ea49c | ||
|
|
1b6d588413 | ||
|
|
334ca21857 | ||
|
|
f58278aaf4 | ||
|
|
e10b9b70ef | ||
|
|
098adf3bdd | ||
|
|
a85c508f69 | ||
|
|
2a713c546b | ||
|
|
f937c1931f | ||
|
|
7a62f01ea5 | ||
|
|
4f22272b0d | ||
|
|
20d7513c73 | ||
|
|
493f8d59e6 | ||
|
|
65a395fa9a | ||
|
|
ca8279ca79 | ||
|
|
19833f0132 | ||
|
|
ad0687a987 | ||
|
|
a51b99216d | ||
|
|
3de07eaf0c | ||
|
|
5fa97e8da8 | ||
|
|
6acc4cc038 | ||
|
|
6a07fe4e99 | ||
|
|
6f05a4b6df | ||
|
|
78f9f4a768 | ||
|
|
46dedb3e13 | ||
|
|
ea5800b322 | ||
|
|
b652196356 | ||
|
|
155a2d2a1e | ||
|
|
f182aa43bb | ||
|
|
f783f22e33 | ||
|
|
6811c57550 | ||
|
|
5739fce607 | ||
|
|
f0fc578fe6 | ||
|
|
7cbc6fb337 | ||
|
|
55c9113177 | ||
|
|
98248d5a7a | ||
|
|
8d5b12a6be | ||
|
|
aa69a52685 | ||
|
|
d5e2a2a00c | ||
|
|
094f514414 | ||
|
|
5abf968748 | ||
|
|
dd455306b2 | ||
|
|
dd4d5b5b0c | ||
|
|
cc7799af38 | ||
|
|
13776b7898 | ||
|
|
67f3b0987a | ||
|
|
6a6b556143 | ||
|
|
3debec1393 | ||
|
|
bde75bb11a | ||
|
|
eff0105c04 | ||
|
|
4bbc53b0ee | ||
|
|
00a62555ec | ||
|
|
d1f085c063 | ||
|
|
73341e51ac | ||
|
|
ed111bf528 | ||
|
|
64966bbecc | ||
|
|
fe895c7c97 | ||
|
|
9c2c9ea949 | ||
|
|
f46b94635d | ||
|
|
b9c8f8b79e | ||
|
|
6ac42dde0d | ||
|
|
7f51ca3dbb | ||
|
|
c050b4225a | ||
|
|
b2073af63a | ||
|
|
a52e4af96d | ||
|
|
35aa3f2207 | ||
|
|
1a808c4642 | ||
|
|
fda2688165 | ||
|
|
7881047432 | ||
|
|
da9281c4a4 | ||
|
|
9cc517e0dd | ||
|
|
d1390a5b78 | ||
|
|
ee4faede38 | ||
|
|
8d96a699b3 | ||
|
|
8cfb7471db | ||
|
|
def9c87837 | ||
|
|
0313ab6d41 | ||
|
|
c5329fdff2 | ||
|
|
a676a6895b | ||
|
|
3b5d7d7d89 | ||
|
|
91f01131b1 | ||
|
|
5fa5226286 | ||
|
|
ae94007227 | ||
|
|
8f425a1bd5 | ||
|
|
743c414e7b | ||
|
|
0fe335efc5 | ||
|
|
36b95aac4b | ||
|
|
b2df70ab58 | ||
|
|
36293d7dd9 | ||
|
|
3ae3e1fce8 | ||
|
|
e5f1fc7478 | ||
|
|
a4f6076da7 | ||
|
|
43726b2620 | ||
|
|
94980ffb49 | ||
|
|
22cc731450 | ||
|
|
d9396373e3 | ||
|
|
48002be135 | ||
|
|
58db83f8f5 | ||
|
|
0243d5b542 | ||
|
|
06230327fa | ||
|
|
ca5c8992f9 | ||
|
|
1038e1c2ef | ||
|
|
e1fe0b3287 | ||
|
|
a0e10a91bf | ||
|
|
272b1aa4bc | ||
|
|
9ef0537b44 | ||
|
|
77f1de742b | ||
|
|
e054cabd41 | ||
|
|
3b95cb5682 | ||
|
|
c89653bd07 | ||
|
|
b90ac2dc07 | ||
|
|
c9998541f0 | ||
|
|
e2b49b3cd3 | ||
|
|
d1e77397c6 | ||
|
|
cc5f5e35e4 | ||
|
|
7183b8a1cd | ||
|
|
b1934fb712 | ||
|
|
a198b6c0d1 | ||
|
|
8b5b2712c8 | ||
|
|
4464392e8e | ||
|
|
a0d3bc31e9 | ||
|
|
ccd6672d1a | ||
|
|
21de6d35dd | ||
|
|
2031ca17e5 | ||
|
|
8b1ce75a57 | ||
|
|
5559726fd7 | ||
|
|
e1a9269921 | ||
|
|
3b6b3ff504 | ||
|
|
aabed94970 | ||
|
|
2d3a3521ba | ||
|
|
a48bd10da0 | ||
|
|
fec9525be4 | ||
|
|
bf2b8e999e | ||
|
|
63c35d2b00 | ||
|
|
1396c68010 | ||
|
|
fcb3d3dec6 | ||
|
|
f54e7f8c9d | ||
|
|
2a89529d7f | ||
|
|
58207325e2 | ||
|
|
e08ab99e8d | ||
|
|
a95f3f33a4 | ||
|
|
b0767c1b1f | ||
|
|
b200e10bc4 | ||
|
|
948905d916 | ||
|
|
04de456373 | ||
|
|
e5ce32e936 | ||
|
|
d7caae30de | ||
|
|
c7e77674a1 |
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,8 +61,7 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: extension_tests::run_clippy
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -26,8 +26,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -72,15 +71,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -94,8 +87,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -114,8 +105,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,8 +20,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -45,8 +44,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
47
.github/workflows/run_tests.yml
vendored
47
.github/workflows/run_tests.yml
vendored
@@ -74,19 +74,12 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
- name: steps::prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_style_failure
|
||||
name: steps::record_style_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -97,8 +90,6 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
outputs:
|
||||
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -119,8 +110,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -167,15 +157,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -189,8 +173,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_mac:
|
||||
needs:
|
||||
@@ -211,8 +193,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -592,24 +573,6 @@ jobs:
|
||||
|
||||
exit $EXIT_CODE
|
||||
shell: bash -euxo pipefail {0}
|
||||
call_autofix:
|
||||
needs:
|
||||
- check_style
|
||||
- run_tests_linux
|
||||
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: run_tests::call_autofix::dispatch_autofix
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -8932,6 +8932,8 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -12648,6 +12650,8 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
|
||||
@@ -20,7 +20,6 @@ Other platforms are not yet available:
|
||||
- [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
|
||||
|
||||
|
||||
@@ -1321,6 +1321,14 @@
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
|
||||
@@ -3,11 +3,11 @@ use agent_client_protocol::{self as acp};
|
||||
use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use gpui::{Entity, SharedString, Task};
|
||||
use language_model::LanguageModelProviderId;
|
||||
use language_model::{IconOrSvg, LanguageModelProviderId};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
|
||||
use ui::{App, IconName};
|
||||
use ui::App;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
@@ -215,7 +215,7 @@ pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<IconName>,
|
||||
pub icon: Option<IconOrSvg>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
||||
@@ -93,7 +93,7 @@ impl LanguageModels {
|
||||
fn refresh_list(&mut self, cx: &App) {
|
||||
let providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -164,7 +164,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -426,7 +426,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -1630,7 +1630,7 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
icon: Some(language_model::IconOrSvg::Icon(ui::IconName::ZedAssistant)),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ use gpui::{
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language_model::IconOrSvg;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
@@ -350,7 +351,11 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(IconOrSvg::Svg(path)) => this.icon_path(path.clone()),
|
||||
Some(IconOrSvg::Icon(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
|
||||
@@ -6,6 +6,7 @@ use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
@@ -70,7 +71,7 @@ impl Render for AcpModelSelectorPopover {
|
||||
.map(|model| model.name.clone())
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon);
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -125,7 +126,14 @@ impl Render for AcpModelSelectorPopover {
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -338,7 +338,13 @@ impl AcpThreadView {
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let placeholder = placeholder_text(agent.name().as_ref(), false);
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let agent_display_name = agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(agent.name()))
|
||||
.unwrap_or_else(|| agent.name());
|
||||
|
||||
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
@@ -377,7 +383,6 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||
@@ -1498,7 +1503,13 @@ impl AcpThreadView {
|
||||
let has_commands = !available_commands.is_empty();
|
||||
self.available_commands.replace(available_commands);
|
||||
|
||||
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(&new_placeholder, window, cx);
|
||||
|
||||
@@ -22,7 +22,8 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -117,7 +118,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -261,9 +262,12 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -416,7 +420,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -103,7 +104,14 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -115,7 +123,7 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::Small),
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
|
||||
@@ -2428,7 +2428,7 @@ impl AgentPanel {
|
||||
let history_is_empty = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
|
||||
@@ -348,7 +348,8 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|_, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
@@ -360,33 +361,52 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
fn to_selected_model(
|
||||
selection: &LanguageModelSelection,
|
||||
registry: &LanguageModelRegistry,
|
||||
cx: &App,
|
||||
) -> Option<language_model::SelectedModel> {
|
||||
let provider_id = LanguageModelProviderId::from(selection.provider.0.clone());
|
||||
|
||||
if registry
|
||||
.provider(&provider_id)
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
{
|
||||
Some(language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
let default = settings.default_model.as_ref().map(to_selected_model);
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let registry_ref = registry.read(cx);
|
||||
|
||||
let default = settings
|
||||
.default_model
|
||||
.as_ref()
|
||||
.and_then(|s| to_selected_model(s, registry_ref, cx));
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
.and_then(|s| to_selected_model(s, registry_ref, cx));
|
||||
let commit_message = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
.and_then(|s| to_selected_model(s, registry_ref, cx));
|
||||
let thread_summary = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
.and_then(|s| to_selected_model(s, registry_ref, cx));
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
.map(to_selected_model)
|
||||
.filter_map(|s| to_selected_model(s, registry_ref, cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.select_default_model(default.as_ref(), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -55,7 +54,7 @@ pub fn language_model_selector(
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.providers();
|
||||
let providers = lm_registry.visible_providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
@@ -76,7 +75,7 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let all = providers
|
||||
let all: Vec<ModelInfo> = providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
@@ -94,7 +93,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
icon: IconOrSvg,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
@@ -124,7 +123,7 @@ pub struct LanguageModelPickerDelegate {
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_refresh_models_task: Task<()>,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
@@ -151,24 +150,42 @@ impl LanguageModelPickerDelegate {
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
on_toggle_favorite: Arc::new(on_toggle_favorite),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|picker, _, event, window, cx| {
|
||||
match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
let query = picker.query(cx);
|
||||
picker.delegate.all_models = Arc::new(all_models(cx));
|
||||
// Update matches will automatically drop the previous task
|
||||
// if we get a provider event again
|
||||
picker.update_matches(query, window, cx)
|
||||
}
|
||||
_ => {}
|
||||
_refresh_models_task: {
|
||||
// Create a channel to signal when models need refreshing
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::unbounded::<()>();
|
||||
|
||||
// Subscribe to registry events and send refresh signals through the channel
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
cx.subscribe(®istry, move |_picker, _, event, _cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
refresh_tx.unbounded_send(()).ok();
|
||||
}
|
||||
},
|
||||
)],
|
||||
language_model::Event::DefaultModelChanged
|
||||
| language_model::Event::InlineAssistantModelChanged
|
||||
| language_model::Event::CommitMessageModelChanged
|
||||
| language_model::Event::ThreadSummaryModelChanged => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Spawn a task that listens for refresh signals and updates the picker
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
while let Some(()) = refresh_rx.next().await {
|
||||
if this
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
picker.delegate.all_models = Arc::new(all_models(cx));
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
// Picker was dropped, exit the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
}
|
||||
@@ -203,7 +220,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -474,7 +491,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -566,7 +583,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.icon(model_info.icon)
|
||||
.map(|this| match &model_info.icon {
|
||||
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
|
||||
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
@@ -702,7 +722,7 @@ mod tests {
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
is_favorite,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,7 +33,8 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -2231,10 +2232,10 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
@@ -2244,6 +2245,13 @@ impl TextThreadEditor {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let provider_icon_element = match provider_icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -2291,7 +2299,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(provider_icon_element)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
title: SharedString,
|
||||
@@ -39,7 +44,7 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<IconName>,
|
||||
icon: Option<ModelIcon>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
@@ -60,7 +65,12 @@ impl ModelSelectorListItem {
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -105,9 +115,12 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
match icon {
|
||||
ModelIcon::Name(icon_name) => Icon::new(icon_name),
|
||||
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.child(Label::new(self.title).truncate()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,7 +13,8 @@ impl ApiKeysWithProviders {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
match icon {
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(name))
|
||||
});
|
||||
div()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
has_configured_providers: bool,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ impl AgentPanelOnboarding {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -51,6 +51,8 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
|
||||
pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -179,7 +181,7 @@ impl CodeContextMenu {
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,26 +1421,6 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
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 as_debug_scenario(&self) -> Option<&DebugScenario> {
|
||||
let Self::DebugScenario(scenario) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(scenario)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
@@ -1446,6 +1428,14 @@ impl CodeActionsItem {
|
||||
Self::DebugScenario(scenario) => scenario.label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
|
||||
Self::Task(_, task) => task.resolved_label.replace("\n", ""),
|
||||
Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
@@ -1555,60 +1545,33 @@ impl CodeActionsMenu {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let colors = cx.theme().colors();
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_debug_scenario(), |this, scenario| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.overflow_x()
|
||||
.child(
|
||||
div()
|
||||
.min_w(CODE_ACTION_MENU_MIN_WIDTH)
|
||||
.max_w(CODE_ACTION_MENU_MAX_WIDTH)
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.when(selected, |this| this.text_color(colors.text_accent))
|
||||
.child(action.menu_label()),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
@@ -1635,4 +1598,42 @@ impl CodeActionsMenu {
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&mut self,
|
||||
max_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
let Some(action) = self.actions.get(self.selected_item) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let label = action.menu_label();
|
||||
let text_system = window.text_system();
|
||||
let mut line_wrapper = text_system.line_wrapper(
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated =
|
||||
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
div()
|
||||
.child(label)
|
||||
.id("code_actions_menu_extended")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,8 @@ impl Settings for EditorSettings {
|
||||
},
|
||||
scrollbar: Scrollbar {
|
||||
show: scrollbar.show.map(Into::into).unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap()
|
||||
&& content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
|
||||
selected_text: scrollbar.selected_text.unwrap(),
|
||||
selected_symbol: scrollbar.selected_symbol.unwrap(),
|
||||
search_results: scrollbar.search_results.unwrap(),
|
||||
|
||||
@@ -1 +1 @@
|
||||
LICENSE-GPL
|
||||
../../LICENSE-GPL
|
||||
@@ -19,6 +19,9 @@ impl Global for GlobalExtensionHostProxy {}
|
||||
///
|
||||
/// This object implements each of the individual proxy types so that their
|
||||
/// methods can be called directly on it.
|
||||
/// Registration function for language model providers.
|
||||
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionHostProxy {
|
||||
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
|
||||
@@ -29,6 +32,7 @@ pub struct ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
|
||||
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
|
||||
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
|
||||
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
|
||||
}
|
||||
|
||||
impl ExtensionHostProxy {
|
||||
@@ -54,6 +58,7 @@ impl ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
debug_adapter_provider_proxy: RwLock::default(),
|
||||
language_model_provider_proxy: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +95,15 @@ impl ExtensionHostProxy {
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_language_model_provider_proxy(
|
||||
&self,
|
||||
proxy: impl ExtensionLanguageModelProviderProxy,
|
||||
) {
|
||||
self.language_model_provider_proxy
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionThemeProxy: Send + Sync + 'static {
|
||||
@@ -446,3 +460,37 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
|
||||
proxy.unregister_debug_locator(locator_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App);
|
||||
}
|
||||
|
||||
impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language_model_provider(provider_id, register_fn, cx)
|
||||
}
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_language_model_provider(provider_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ pub struct ExtensionManifest {
|
||||
pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub language_model_providers: BTreeMap<Arc<str>, LanguageModelProviderManifestEntry>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
@@ -288,6 +290,16 @@ pub struct DebugAdapterManifestEntry {
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct DebugLocatorManifestEntry {}
|
||||
|
||||
/// Manifest entry for a language model provider.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageModelProviderManifestEntry {
|
||||
/// Display name for the provider.
|
||||
pub name: String,
|
||||
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
|
||||
let extension_name = extension_dir
|
||||
@@ -358,6 +370,7 @@ fn manifest_from_old_manifest(
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +404,7 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,21 @@ async fn copy_extension_resources(
|
||||
}
|
||||
}
|
||||
|
||||
for (_, provider_entry) in &manifest.language_model_providers {
|
||||
if let Some(icon_path) = &provider_entry.icon {
|
||||
let source_icon = extension_path.join(icon_path);
|
||||
let dest_icon = output_dir.join(icon_path);
|
||||
|
||||
// Create parent directory if needed
|
||||
if let Some(parent) = dest_icon.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(&source_icon, &dest_icon)
|
||||
.with_context(|| format!("failed to copy LLM provider icon '{}'", icon_path))?;
|
||||
}
|
||||
}
|
||||
|
||||
if !manifest.languages.is_empty() {
|
||||
let output_languages_dir = output_dir.join("languages");
|
||||
fs::create_dir_all(&output_languages_dir)?;
|
||||
|
||||
@@ -148,6 +148,7 @@ fn manifest() -> ExtensionManifest {
|
||||
)],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -196,6 +197,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -376,6 +378,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ use project::{
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle};
|
||||
use std::future::Future;
|
||||
@@ -2579,25 +2579,26 @@ impl GitPanel {
|
||||
is_using_legacy_zed_pro: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> String {
|
||||
const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
// Remove this once we stop supporting legacy Zed Pro
|
||||
// In legacy Zed Pro, Git commit summary generation did not count as a
|
||||
// prompt. If the user changes the prompt, our classification will fail,
|
||||
// meaning that users will be charged for generating commit messages.
|
||||
if is_using_legacy_zed_pro {
|
||||
return DEFAULT_PROMPT.to_string();
|
||||
return BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
}
|
||||
|
||||
let load = async {
|
||||
let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?;
|
||||
store
|
||||
.update(cx, |s, cx| s.load(PromptId::CommitMessage, cx))
|
||||
.update(cx, |s, cx| {
|
||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string())
|
||||
load.await
|
||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
||||
}
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
|
||||
@@ -316,6 +316,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "next" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let next_idx = (idx + 1) % group_ids.len();
|
||||
@@ -340,6 +341,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "previous" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let prev_idx = if idx == 0 {
|
||||
@@ -361,12 +363,9 @@ impl SystemWindowTabController {
|
||||
|
||||
/// Get all tabs in the same window.
|
||||
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
|
||||
let tab_group = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
|
||||
|
||||
self.tab_groups.get(&tab_group)
|
||||
self.tab_groups
|
||||
.values()
|
||||
.find(|tabs| tabs.iter().any(|tab| tab.id == id))
|
||||
}
|
||||
|
||||
/// Initialize the visibility of the system window tab controller.
|
||||
@@ -441,7 +440,7 @@ impl SystemWindowTabController {
|
||||
/// Insert a tab into a tab group.
|
||||
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
|
||||
let mut controller = cx.global_mut::<SystemWindowTabController>();
|
||||
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
|
||||
let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -504,16 +503,14 @@ impl SystemWindowTabController {
|
||||
return;
|
||||
};
|
||||
|
||||
let initial_tabs_len = initial_tabs.len();
|
||||
let mut all_tabs = initial_tabs.clone();
|
||||
for tabs in controller.tab_groups.values() {
|
||||
all_tabs.extend(
|
||||
tabs.iter()
|
||||
.filter(|tab| !initial_tabs.contains(tab))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
for (_, mut tabs) in controller.tab_groups.drain() {
|
||||
tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
|
||||
all_tabs.extend(tabs);
|
||||
}
|
||||
|
||||
controller.tab_groups.clear();
|
||||
controller.tab_groups.insert(0, all_tabs);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
MetalRenderer::new(context, transparent)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,8 +152,13 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
// We already present at display sync with the display link
|
||||
// This allows to use direct-to-display even in window mode
|
||||
layer.set_display_sync_enabled(false);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -352,8 +357,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -128,22 +128,21 @@ impl LineWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
/// Determines if a line should be truncated based on its width.
|
||||
pub fn should_truncate_line(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
line: &str,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
let mut suffix_width = truncation_suffix
|
||||
let suffix_width = truncation_suffix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut char_indices = line.char_indices();
|
||||
let mut truncate_ix = 0;
|
||||
for (ix, c) in char_indices {
|
||||
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
@@ -152,16 +151,32 @@ impl LineWrapper {
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
|
||||
return (result, Cow::Owned(runs));
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
|
||||
(line, Cow::Borrowed(runs))
|
||||
None
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_suffix)
|
||||
{
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Any character in this list should be treated as a word character,
|
||||
|
||||
@@ -797,11 +797,26 @@ pub enum AuthenticateError {
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Either a built-in icon name or a path to an external SVG.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IconOrSvg {
|
||||
/// A built-in icon from Zed's icon set.
|
||||
Icon(IconName),
|
||||
/// Path to a custom SVG icon file.
|
||||
Svg(SharedString),
|
||||
}
|
||||
|
||||
impl Default for IconOrSvg {
|
||||
fn default() -> Self {
|
||||
Self::Icon(IconName::ZedAssistant)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelProvider: 'static {
|
||||
fn id(&self) -> LanguageModelProviderId;
|
||||
fn name(&self) -> LanguageModelProviderName;
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ZedAssistant
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::default()
|
||||
}
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
@@ -820,7 +835,7 @@ pub trait LanguageModelProvider: 'static {
|
||||
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub enum ConfigurationViewTargetAgent {
|
||||
#[default]
|
||||
ZedAgent,
|
||||
|
||||
@@ -2,12 +2,16 @@ use crate::{
|
||||
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderState,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
|
||||
/// Function type for checking if a built-in provider should be hidden.
|
||||
/// Returns Some(extension_id) if the provider should be hidden when that extension is installed.
|
||||
pub type BuiltinProviderHidingFn = Box<dyn Fn(&str) -> Option<&'static str> + Send + Sync>;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||
cx.set_global(GlobalLanguageModelRegistry(registry));
|
||||
@@ -48,6 +52,11 @@ pub struct LanguageModelRegistry {
|
||||
thread_summary_model: Option<ConfiguredModel>,
|
||||
providers: BTreeMap<LanguageModelProviderId, Arc<dyn LanguageModelProvider>>,
|
||||
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
|
||||
/// Set of installed extension IDs that provide language models.
|
||||
/// Used to determine which built-in providers should be hidden.
|
||||
installed_llm_extension_ids: HashSet<Arc<str>>,
|
||||
/// Function to check if a built-in provider should be hidden by an extension.
|
||||
builtin_provider_hiding_fn: Option<BuiltinProviderHidingFn>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -104,6 +113,8 @@ pub enum Event {
|
||||
ProviderStateChanged(LanguageModelProviderId),
|
||||
AddedProvider(LanguageModelProviderId),
|
||||
RemovedProvider(LanguageModelProviderId),
|
||||
/// Emitted when provider visibility changes due to extension install/uninstall.
|
||||
ProvidersChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for LanguageModelRegistry {}
|
||||
@@ -183,6 +194,60 @@ impl LanguageModelRegistry {
|
||||
providers
|
||||
}
|
||||
|
||||
/// Returns providers, filtering out hidden built-in providers.
|
||||
pub fn visible_providers(&self) -> Vec<Arc<dyn LanguageModelProvider>> {
|
||||
self.providers()
|
||||
.into_iter()
|
||||
.filter(|p| !self.should_hide_provider(&p.id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sets the function used to check if a built-in provider should be hidden.
|
||||
pub fn set_builtin_provider_hiding_fn(&mut self, hiding_fn: BuiltinProviderHidingFn) {
|
||||
self.builtin_provider_hiding_fn = Some(hiding_fn);
|
||||
}
|
||||
|
||||
/// Called when an extension is installed/loaded.
|
||||
/// If the extension provides language models, track it so we can hide the corresponding built-in.
|
||||
pub fn extension_installed(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
|
||||
if self.installed_llm_extension_ids.insert(extension_id) {
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when an extension is uninstalled/unloaded.
|
||||
pub fn extension_uninstalled(&mut self, extension_id: &str, cx: &mut Context<Self>) {
|
||||
if self.installed_llm_extension_ids.remove(extension_id) {
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the set of installed LLM extension IDs.
|
||||
pub fn sync_installed_llm_extensions(
|
||||
&mut self,
|
||||
extension_ids: HashSet<Arc<str>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if extension_ids != self.installed_llm_extension_ids {
|
||||
self.installed_llm_extension_ids = extension_ids;
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a provider should be hidden from the UI.
|
||||
/// Built-in providers are hidden when their corresponding extension is installed.
|
||||
pub fn should_hide_provider(&self, provider_id: &LanguageModelProviderId) -> bool {
|
||||
if let Some(ref hiding_fn) = self.builtin_provider_hiding_fn {
|
||||
if let Some(extension_id) = hiding_fn(&provider_id.0) {
|
||||
return self.installed_llm_extension_ids.contains(extension_id);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn configuration_error(
|
||||
&self,
|
||||
model: Option<ConfiguredModel>,
|
||||
@@ -416,4 +481,132 @@ mod tests {
|
||||
let providers = registry.read(cx).providers();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_provider_hiding_on_extension_install(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let provider_id = provider.id();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert_eq!(visible.len(), 1);
|
||||
assert_eq!(visible[0].id(), provider_id);
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_installed("fake-extension".into(), cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert!(visible.is_empty());
|
||||
|
||||
let all = registry.read(cx).providers();
|
||||
assert_eq!(all.len(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_provider_unhiding_on_extension_uninstall(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let provider_id = provider.id();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
registry.extension_installed("fake-extension".into(), cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert!(visible.is_empty());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_uninstalled("fake-extension", cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert_eq!(visible.len(), 1);
|
||||
assert_eq!(visible[0].id(), provider_id);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_should_hide_provider(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "anthropic" {
|
||||
Some("anthropic")
|
||||
} else if id == "openai" {
|
||||
Some("openai")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
registry.extension_installed("anthropic".into(), cx);
|
||||
});
|
||||
|
||||
let registry_read = registry.read(cx);
|
||||
|
||||
assert!(registry_read.should_hide_provider(&LanguageModelProviderId("anthropic".into())));
|
||||
|
||||
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("openai".into())));
|
||||
|
||||
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("unknown".into())));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_sync_installed_llm_extensions(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let mut extension_ids = HashSet::default();
|
||||
extension_ids.insert(Arc::from("fake-extension"));
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(extension_ids, cx);
|
||||
});
|
||||
|
||||
assert!(registry.read(cx).visible_providers().is_empty());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(HashSet::default(), cx);
|
||||
});
|
||||
|
||||
assert_eq!(registry.read(cx).visible_providers().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ convert_case.workspace = true
|
||||
copilot.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
|
||||
67
crates/language_models/src/extension.rs
Normal file
67
crates/language_models/src/extension.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use collections::HashMap;
|
||||
use extension::{
|
||||
ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration,
|
||||
};
|
||||
use gpui::{App, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
/// Maps built-in provider IDs to their corresponding extension IDs.
|
||||
/// When an extension with this ID is installed, the built-in provider should be hidden.
|
||||
static BUILTIN_TO_EXTENSION_MAP: LazyLock<HashMap<&'static str, &'static str>> =
|
||||
LazyLock::new(|| {
|
||||
let mut map = HashMap::default();
|
||||
map.insert("anthropic", "anthropic");
|
||||
map.insert("openai", "openai");
|
||||
map.insert("google", "google-ai");
|
||||
map.insert("openrouter", "openrouter");
|
||||
map.insert("copilot_chat", "copilot-chat");
|
||||
map
|
||||
});
|
||||
|
||||
/// Returns the extension ID that should hide the given built-in provider.
|
||||
pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> {
|
||||
BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied()
|
||||
}
|
||||
|
||||
/// Proxy that registers extension language model providers with the LanguageModelRegistry.
|
||||
pub struct LanguageModelProviderRegistryProxy {
|
||||
registry: Entity<LanguageModelRegistry>,
|
||||
}
|
||||
|
||||
impl LanguageModelProviderRegistryProxy {
|
||||
pub fn new(registry: Entity<LanguageModelRegistry>) -> Self {
|
||||
Self { registry }
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
_provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
register_fn(cx);
|
||||
}
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
|
||||
self.registry.update(cx, |registry, cx| {
|
||||
registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the extension language model provider proxy.
|
||||
/// This must be called BEFORE extension_host::init to ensure the proxy is available
|
||||
/// when extensions try to register their language model providers.
|
||||
pub fn init_proxy(cx: &mut App) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
registry.update(cx, |registry, _cx| {
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(extension_for_builtin_provider));
|
||||
});
|
||||
|
||||
proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry));
|
||||
}
|
||||
@@ -7,14 +7,16 @@ use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
pub mod extension;
|
||||
pub mod provider;
|
||||
mod settings;
|
||||
|
||||
pub use crate::extension::init_proxy as init_extension_proxy;
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||
use crate::provider::cloud::CloudLanguageModelProvider;
|
||||
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
||||
use crate::provider::google::GoogleLanguageModelProvider;
|
||||
pub use crate::provider::google::GoogleLanguageModelProvider;
|
||||
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
||||
pub use crate::provider::mistral::MistralLanguageModelProvider;
|
||||
use crate::provider::ollama::OllamaLanguageModelProvider;
|
||||
@@ -31,6 +33,61 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
register_language_model_providers(registry, user_store, client.clone(), cx);
|
||||
});
|
||||
|
||||
// Subscribe to extension store events to track LLM extension installations
|
||||
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
|
||||
cx.subscribe(&extension_store, {
|
||||
let registry = registry.clone();
|
||||
move |extension_store, event, cx| {
|
||||
match event {
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => {
|
||||
// Check if this extension has language_model_providers
|
||||
if let Some(manifest) = extension_store
|
||||
.read(cx)
|
||||
.extension_manifest_for_id(extension_id)
|
||||
{
|
||||
if !manifest.language_model_providers.is_empty() {
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_installed(extension_id.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
extension_host::Event::ExtensionUninstalled(extension_id) => {
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_uninstalled(extension_id, cx);
|
||||
});
|
||||
}
|
||||
extension_host::Event::ExtensionsUpdated => {
|
||||
// Re-sync installed extensions on bulk updates
|
||||
let mut new_ids = HashSet::default();
|
||||
for (extension_id, entry) in extension_store.read(cx).installed_extensions()
|
||||
{
|
||||
if !entry.manifest.language_model_providers.is_empty() {
|
||||
new_ids.insert(extension_id.clone());
|
||||
}
|
||||
}
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(new_ids, cx);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Initialize with currently installed extensions
|
||||
registry.update(cx, |registry, cx| {
|
||||
let mut initial_ids = HashSet::default();
|
||||
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
|
||||
if !entry.manifest.language_model_providers.is_empty() {
|
||||
initial_ids.insert(extension_id.clone());
|
||||
}
|
||||
}
|
||||
registry.sync_installed_llm_extensions(initial_ids, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
|
||||
.openai_compatible
|
||||
.keys()
|
||||
|
||||
@@ -8,7 +8,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
|
||||
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
|
||||
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
@@ -125,8 +125,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiAnthropic
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiAnthropic)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -30,7 +30,7 @@ use gpui::{
|
||||
use gpui_tokio::Tokio;
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, EnvVar, LanguageModel, LanguageModelCacheConfiguration,
|
||||
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
@@ -426,8 +426,8 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiBedrock
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiBedrock)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -996,7 +996,7 @@ pub fn get_bedrock_tokens(
|
||||
}
|
||||
}
|
||||
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// Tiktoken doesn't support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
|
||||
.map(|tokens| (tokens + tokens_from_images) as u64)
|
||||
|
||||
@@ -19,7 +19,7 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Ta
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
|
||||
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
@@ -304,8 +304,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiZed
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiZed)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -18,12 +18,12 @@ use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
|
||||
use http_client::StatusCode;
|
||||
use language::language_settings::all_language_settings;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
|
||||
StopReason, TokenUsage,
|
||||
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
|
||||
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
|
||||
MessageContent, RateLimiter, Role, StopReason, TokenUsage,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::prelude::*;
|
||||
@@ -104,8 +104,8 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Copilot
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::Copilot)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -127,8 +127,8 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiDeepSeek
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiDeepSeek)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
@@ -164,8 +164,8 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiGoogle
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiGoogle)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -707,7 +707,7 @@ pub fn count_google_tokens(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// Tiktoken doesn't support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages).map(|tokens| tokens as u64)
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ use language_model::{
|
||||
StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
@@ -175,8 +175,8 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiLmStudio
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiLmStudio)
|
||||
}
|
||||
|
||||
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -176,8 +176,8 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiMistral
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiMistral)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{Stream, TryFutureExt, stream};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
|
||||
@@ -221,8 +221,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOllama
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOllama)
|
||||
}
|
||||
|
||||
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -249,33 +249,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||
}
|
||||
|
||||
// Override with available models from settings
|
||||
for setting_model in &OllamaLanguageModelProvider::settings(cx).available_models {
|
||||
let setting_base = setting_model.name.split(':').next().unwrap();
|
||||
if let Some(model) = models
|
||||
.values_mut()
|
||||
.find(|m| m.name.split(':').next().unwrap() == setting_base)
|
||||
{
|
||||
model.max_tokens = setting_model.max_tokens;
|
||||
model.display_name = setting_model.display_name.clone();
|
||||
model.keep_alive = setting_model.keep_alive.clone();
|
||||
model.supports_tools = setting_model.supports_tools;
|
||||
model.supports_vision = setting_model.supports_images;
|
||||
model.supports_thinking = setting_model.supports_thinking;
|
||||
} else {
|
||||
models.insert(
|
||||
setting_model.name.clone(),
|
||||
ollama::Model {
|
||||
name: setting_model.name.clone(),
|
||||
display_name: setting_model.display_name.clone(),
|
||||
max_tokens: setting_model.max_tokens,
|
||||
keep_alive: setting_model.keep_alive.clone(),
|
||||
supports_tools: setting_model.supports_tools,
|
||||
supports_vision: setting_model.supports_images,
|
||||
supports_thinking: setting_model.supports_thinking,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
merge_settings_into_models(&mut models, &settings.available_models);
|
||||
|
||||
let mut models = models
|
||||
.into_values()
|
||||
@@ -921,6 +895,35 @@ impl Render for ConfigurationView {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_settings_into_models(
|
||||
models: &mut HashMap<String, ollama::Model>,
|
||||
available_models: &[AvailableModel],
|
||||
) {
|
||||
for setting_model in available_models {
|
||||
if let Some(model) = models.get_mut(&setting_model.name) {
|
||||
model.max_tokens = setting_model.max_tokens;
|
||||
model.display_name = setting_model.display_name.clone();
|
||||
model.keep_alive = setting_model.keep_alive.clone();
|
||||
model.supports_tools = setting_model.supports_tools;
|
||||
model.supports_vision = setting_model.supports_images;
|
||||
model.supports_thinking = setting_model.supports_thinking;
|
||||
} else {
|
||||
models.insert(
|
||||
setting_model.name.clone(),
|
||||
ollama::Model {
|
||||
name: setting_model.name.clone(),
|
||||
display_name: setting_model.display_name.clone(),
|
||||
max_tokens: setting_model.max_tokens,
|
||||
keep_alive: setting_model.keep_alive.clone(),
|
||||
supports_tools: setting_model.supports_tools,
|
||||
supports_vision: setting_model.supports_images,
|
||||
supports_thinking: setting_model.supports_thinking,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
|
||||
ollama::OllamaTool::Function {
|
||||
function: OllamaFunctionTool {
|
||||
@@ -930,3 +933,83 @@ fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_merge_settings_preserves_display_names_for_similar_models() {
|
||||
// Regression test for https://github.com/zed-industries/zed/issues/43646
|
||||
// When multiple models share the same base name (e.g., qwen2.5-coder:1.5b and qwen2.5-coder:3b),
|
||||
// each model should get its own display_name from settings, not a random one.
|
||||
|
||||
let mut models: HashMap<String, ollama::Model> = HashMap::new();
|
||||
models.insert(
|
||||
"qwen2.5-coder:1.5b".to_string(),
|
||||
ollama::Model {
|
||||
name: "qwen2.5-coder:1.5b".to_string(),
|
||||
display_name: None,
|
||||
max_tokens: 4096,
|
||||
keep_alive: None,
|
||||
supports_tools: None,
|
||||
supports_vision: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
);
|
||||
models.insert(
|
||||
"qwen2.5-coder:3b".to_string(),
|
||||
ollama::Model {
|
||||
name: "qwen2.5-coder:3b".to_string(),
|
||||
display_name: None,
|
||||
max_tokens: 4096,
|
||||
keep_alive: None,
|
||||
supports_tools: None,
|
||||
supports_vision: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
);
|
||||
|
||||
let available_models = vec![
|
||||
AvailableModel {
|
||||
name: "qwen2.5-coder:1.5b".to_string(),
|
||||
display_name: Some("QWEN2.5 Coder 1.5B".to_string()),
|
||||
max_tokens: 5000,
|
||||
keep_alive: None,
|
||||
supports_tools: Some(true),
|
||||
supports_images: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
AvailableModel {
|
||||
name: "qwen2.5-coder:3b".to_string(),
|
||||
display_name: Some("QWEN2.5 Coder 3B".to_string()),
|
||||
max_tokens: 6000,
|
||||
keep_alive: None,
|
||||
supports_tools: Some(true),
|
||||
supports_images: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
];
|
||||
|
||||
merge_settings_into_models(&mut models, &available_models);
|
||||
|
||||
let model_1_5b = models
|
||||
.get("qwen2.5-coder:1.5b")
|
||||
.expect("1.5b model missing");
|
||||
let model_3b = models.get("qwen2.5-coder:3b").expect("3b model missing");
|
||||
|
||||
assert_eq!(
|
||||
model_1_5b.display_name,
|
||||
Some("QWEN2.5 Coder 1.5B".to_string()),
|
||||
"1.5b model should have its own display_name"
|
||||
);
|
||||
assert_eq!(model_1_5b.max_tokens, 5000);
|
||||
|
||||
assert_eq!(
|
||||
model_3b.display_name,
|
||||
Some("QWEN2.5 Coder 3B".to_string()),
|
||||
"3b model should have its own display_name"
|
||||
);
|
||||
assert_eq!(model_3b.max_tokens, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -122,8 +122,8 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenAi
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenAi)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
@@ -133,8 +133,8 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenAiCompat
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenAiCompat)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -180,8 +180,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenRouter
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenRouter)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
|
||||
@@ -117,8 +117,8 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiVZero
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiVZero)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
@@ -118,8 +118,8 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiXAi
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiXAi)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -127,6 +127,16 @@ impl LanguageServerState {
|
||||
return menu;
|
||||
};
|
||||
|
||||
let server_versions = self
|
||||
.lsp_store
|
||||
.update(cx, |lsp_store, _| {
|
||||
lsp_store
|
||||
.language_server_statuses()
|
||||
.map(|(server_id, status)| (server_id, status.server_version.clone()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut first_button_encountered = false;
|
||||
for item in &self.items {
|
||||
if let LspMenuItem::ToggleServersButton { restart } = item {
|
||||
@@ -254,6 +264,22 @@ impl LanguageServerState {
|
||||
};
|
||||
|
||||
let server_name = server_info.name.clone();
|
||||
let server_version = server_versions
|
||||
.get(&server_info.id)
|
||||
.and_then(|version| version.clone());
|
||||
|
||||
let tooltip_text = match (&server_version, &message) {
|
||||
(None, None) => None,
|
||||
(Some(version), None) => {
|
||||
Some(SharedString::from(format!("Version: {}", version.as_ref())))
|
||||
}
|
||||
(None, Some(message)) => Some(message.clone()),
|
||||
(Some(version), Some(message)) => Some(SharedString::from(format!(
|
||||
"Version: {}\n\n{}",
|
||||
version.as_ref(),
|
||||
message.as_ref()
|
||||
))),
|
||||
};
|
||||
menu = menu.item(ContextMenuItem::custom_entry(
|
||||
move |_, _| {
|
||||
h_flex()
|
||||
@@ -355,11 +381,11 @@ impl LanguageServerState {
|
||||
}
|
||||
}
|
||||
},
|
||||
message.map(|server_message| {
|
||||
tooltip_text.map(|tooltip_text| {
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Right,
|
||||
DocumentationEdge::Bottom,
|
||||
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
|
||||
DocumentationEdge::Top,
|
||||
Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
|
||||
)
|
||||
}),
|
||||
));
|
||||
|
||||
@@ -330,6 +330,8 @@ impl LspLogView {
|
||||
let server_info = format!(
|
||||
"* Server: {NAME} (id {ID})
|
||||
|
||||
* Version: {VERSION}
|
||||
|
||||
* Binary: {BINARY}
|
||||
|
||||
* Registered workspace folders:
|
||||
@@ -340,6 +342,12 @@ impl LspLogView {
|
||||
* Configuration: {CONFIGURATION}",
|
||||
NAME = info.status.name,
|
||||
ID = info.id,
|
||||
VERSION = info
|
||||
.status
|
||||
.server_version
|
||||
.as_ref()
|
||||
.map(|version| version.as_ref())
|
||||
.unwrap_or("Unknown"),
|
||||
BINARY = info
|
||||
.status
|
||||
.binary
|
||||
@@ -1334,6 +1342,7 @@ impl ServerInfo {
|
||||
capabilities: server.capabilities(),
|
||||
status: LanguageServerStatus {
|
||||
name: server.name(),
|
||||
server_version: server.version(),
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
|
||||
@@ -89,6 +89,7 @@ pub struct LanguageServer {
|
||||
outbound_tx: channel::Sender<String>,
|
||||
notification_tx: channel::Sender<NotificationSerializer>,
|
||||
name: LanguageServerName,
|
||||
version: Option<SharedString>,
|
||||
process_name: Arc<str>,
|
||||
binary: LanguageServerBinary,
|
||||
capabilities: RwLock<ServerCapabilities>,
|
||||
@@ -501,6 +502,7 @@ impl LanguageServer {
|
||||
response_handlers,
|
||||
io_handlers,
|
||||
name: server_name,
|
||||
version: None,
|
||||
process_name: binary
|
||||
.path
|
||||
.file_name()
|
||||
@@ -882,7 +884,9 @@ impl LanguageServer {
|
||||
window: Some(WindowClientCapabilities {
|
||||
work_done_progress: Some(true),
|
||||
show_message: Some(ShowMessageRequestClientCapabilities {
|
||||
message_action_item: None,
|
||||
message_action_item: Some(MessageActionItemCapabilities {
|
||||
additional_properties_support: Some(true),
|
||||
}),
|
||||
}),
|
||||
..WindowClientCapabilities::default()
|
||||
}),
|
||||
@@ -923,6 +927,7 @@ impl LanguageServer {
|
||||
)
|
||||
})?;
|
||||
if let Some(info) = response.server_info {
|
||||
self.version = info.version.map(SharedString::from);
|
||||
self.process_name = info.name.into();
|
||||
}
|
||||
self.capabilities = RwLock::new(response.capabilities);
|
||||
@@ -1153,6 +1158,11 @@ impl LanguageServer {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
/// Get the version of the running language server.
|
||||
pub fn version(&self) -> Option<SharedString> {
|
||||
self.version.clone()
|
||||
}
|
||||
|
||||
pub fn process_name(&self) -> &str {
|
||||
&self.process_name
|
||||
}
|
||||
|
||||
@@ -50,7 +50,13 @@ impl Settings for OutlinePanelSettings {
|
||||
dock: panel.dock.unwrap(),
|
||||
file_icons: panel.file_icons.unwrap(),
|
||||
folder_icons: panel.folder_icons.unwrap(),
|
||||
git_status: panel.git_status.unwrap(),
|
||||
git_status: panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -128,6 +128,7 @@ use util::{
|
||||
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
|
||||
paths::{PathStyle, SanitizedPath},
|
||||
post_inc,
|
||||
redact::redact_command,
|
||||
rel_path::RelPath,
|
||||
};
|
||||
|
||||
@@ -577,9 +578,12 @@ impl LocalLspStore {
|
||||
},
|
||||
},
|
||||
);
|
||||
log::error!("Failed to start language server {server_name:?}: {err:?}");
|
||||
log::error!(
|
||||
"Failed to start language server {server_name:?}: {}",
|
||||
redact_command(&format!("{err:?}"))
|
||||
);
|
||||
if !log.is_empty() {
|
||||
log::error!("server stderr: {log}");
|
||||
log::error!("server stderr: {}", redact_command(&log));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -3860,6 +3864,7 @@ pub enum LspStoreEvent {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LanguageServerStatus {
|
||||
pub name: LanguageServerName,
|
||||
pub server_version: Option<SharedString>,
|
||||
pub pending_work: BTreeMap<ProgressToken, LanguageServerProgress>,
|
||||
pub has_pending_diagnostic_updates: bool,
|
||||
pub progress_tokens: HashSet<ProgressToken>,
|
||||
@@ -8350,6 +8355,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name,
|
||||
server_version: None,
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
@@ -9385,6 +9391,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name: server_name.clone(),
|
||||
server_version: None,
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
@@ -11415,6 +11422,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name: language_server.name(),
|
||||
server_version: language_server.version(),
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
|
||||
@@ -332,6 +332,10 @@ impl GoToDiagnosticSeverityFilter {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not git integration is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
pub enabled: GitEnabledSettings,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -361,6 +365,18 @@ pub struct GitSettings {
|
||||
pub path_style: GitPathStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Whether git integration is enabled for showing git status.
|
||||
///
|
||||
/// Default: true
|
||||
pub status: bool,
|
||||
/// Whether git integration is enabled for showing diffs.
|
||||
///
|
||||
/// Default: true
|
||||
pub diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
pub enum GitPathStyle {
|
||||
#[default]
|
||||
@@ -502,7 +518,14 @@ impl Settings for ProjectSettings {
|
||||
let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
|
||||
|
||||
let git = content.git.as_ref().unwrap();
|
||||
let git_enabled = {
|
||||
GitEnabledSettings {
|
||||
status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
|
||||
diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
|
||||
}
|
||||
};
|
||||
let git_settings = GitSettings {
|
||||
enabled: git_enabled,
|
||||
git_gutter: git.git_gutter.unwrap(),
|
||||
gutter_debounce: git.gutter_debounce.unwrap_or_default(),
|
||||
inline_blame: {
|
||||
|
||||
@@ -92,7 +92,13 @@ impl Settings for ProjectPanelSettings {
|
||||
entry_spacing: project_panel.entry_spacing.unwrap(),
|
||||
file_icons: project_panel.file_icons.unwrap(),
|
||||
folder_icons: project_panel.folder_icons.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: project_panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: project_panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -28,6 +28,11 @@ parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod prompts;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use strum::{EnumIter, IntoEnumIterator as _};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
@@ -51,11 +52,51 @@ pub struct PromptMetadata {
|
||||
pub saved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PromptMetadata {
|
||||
fn builtin(builtin: BuiltInPrompt) -> Self {
|
||||
Self {
|
||||
id: PromptId::BuiltIn(builtin),
|
||||
title: Some(builtin.title().into()),
|
||||
default: false,
|
||||
saved_at: DateTime::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in prompts that have default content and can be customized by users.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
|
||||
pub enum BuiltInPrompt {
|
||||
CommitMessage,
|
||||
}
|
||||
|
||||
impl BuiltInPrompt {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => "Commit message",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default content for this built-in prompt.
|
||||
pub fn default_content(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => include_str!("../../git_ui/src/commit_message_prompt.txt"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BuiltInPrompt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CommitMessage => write!(f, "Commit message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum PromptId {
|
||||
User { uuid: UserPromptId },
|
||||
CommitMessage,
|
||||
BuiltIn(BuiltInPrompt),
|
||||
}
|
||||
|
||||
impl PromptId {
|
||||
@@ -63,31 +104,37 @@ impl PromptId {
|
||||
UserPromptId::new().into()
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Option<UserPromptId> {
|
||||
pub fn as_user(&self) -> Option<UserPromptId> {
|
||||
match self {
|
||||
Self::User { uuid } => Some(*uuid),
|
||||
_ => None,
|
||||
Self::BuiltIn { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_built_in(&self) -> Option<BuiltInPrompt> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::BuiltIn(builtin) => Some(*builtin),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_built_in(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } => false,
|
||||
Self::CommitMessage => true,
|
||||
}
|
||||
matches!(self, Self::BuiltIn { .. })
|
||||
}
|
||||
|
||||
pub fn can_edit(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } | Self::CommitMessage => true,
|
||||
Self::User { .. } => true,
|
||||
Self::BuiltIn(builtin) => match builtin {
|
||||
BuiltInPrompt::CommitMessage => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_content(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::CommitMessage => Some(include_str!("../../git_ui/src/commit_message_prompt.txt")),
|
||||
}
|
||||
impl From<BuiltInPrompt> for PromptId {
|
||||
fn from(builtin: BuiltInPrompt) -> Self {
|
||||
PromptId::BuiltIn(builtin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +164,7 @@ impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PromptId::User { uuid } => write!(f, "{}", uuid.0),
|
||||
PromptId::CommitMessage => write!(f, "Commit message"),
|
||||
PromptId::BuiltIn(builtin) => write!(f, "{}", builtin),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +197,16 @@ impl MetadataCache {
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||
}
|
||||
|
||||
// Insert all the built-in prompts that were not customized by the user
|
||||
for builtin in BuiltInPrompt::iter() {
|
||||
let builtin_id = PromptId::BuiltIn(builtin);
|
||||
if !cache.metadata_by_id.contains_key(&builtin_id) {
|
||||
let metadata = PromptMetadata::builtin(builtin);
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(builtin_id, metadata);
|
||||
}
|
||||
}
|
||||
cache.sort();
|
||||
Ok(cache)
|
||||
}
|
||||
@@ -198,10 +255,6 @@ impl PromptStore {
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
|
||||
metadata.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
bodies.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
@@ -294,7 +347,16 @@ impl PromptStore {
|
||||
let bodies = self.bodies;
|
||||
cx.background_spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into();
|
||||
let mut prompt: String = match bodies.get(&txn, &id)? {
|
||||
Some(body) => body.into(),
|
||||
None => {
|
||||
if let Some(built_in) = id.as_built_in() {
|
||||
built_in.default_content().into()
|
||||
} else {
|
||||
anyhow::bail!("prompt not found")
|
||||
}
|
||||
}
|
||||
};
|
||||
LineEnding::normalize(&mut prompt);
|
||||
Ok(prompt)
|
||||
})
|
||||
@@ -339,11 +401,6 @@ impl PromptStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of prompts in the store.
|
||||
pub fn prompt_count(&self) -> usize {
|
||||
self.metadata_cache.read().metadata.len()
|
||||
}
|
||||
|
||||
pub fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
|
||||
}
|
||||
@@ -412,23 +469,38 @@ impl PromptStore {
|
||||
return Task::ready(Err(anyhow!("this prompt cannot be edited")));
|
||||
}
|
||||
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
let body = body.to_string();
|
||||
let is_default_content = id
|
||||
.as_built_in()
|
||||
.is_some_and(|builtin| body.trim() == builtin.default_content().trim());
|
||||
|
||||
let metadata = if let Some(builtin) = id.as_built_in() {
|
||||
PromptMetadata::builtin(builtin)
|
||||
} else {
|
||||
PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
}
|
||||
};
|
||||
self.metadata_cache.write().insert(prompt_metadata.clone());
|
||||
|
||||
self.metadata_cache.write().insert(metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
let metadata_db = self.metadata;
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
bodies.put(&mut txn, &id, &body.to_string())?;
|
||||
if is_default_content {
|
||||
metadata_db.delete(&mut txn, &id)?;
|
||||
bodies.delete(&mut txn, &id)?;
|
||||
} else {
|
||||
metadata_db.put(&mut txn, &id, &metadata)?;
|
||||
bodies.put(&mut txn, &id, &body)?;
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
@@ -490,3 +562,122 @@ impl PromptStore {
|
||||
pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_built_in_prompt_load_save(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("prompts-db");
|
||||
|
||||
let store = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
|
||||
let store = cx.new(|_cx| store);
|
||||
|
||||
let commit_message_id = PromptId::BuiltIn(BuiltInPrompt::CommitMessage);
|
||||
|
||||
let loaded_content = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut expected_content = BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content);
|
||||
assert_eq!(
|
||||
loaded_content.trim(),
|
||||
expected_content.trim(),
|
||||
"Loading a built-in prompt not in DB should return default content"
|
||||
);
|
||||
|
||||
let metadata = store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata.is_some(),
|
||||
"Built-in prompt should always have metadata"
|
||||
);
|
||||
assert!(
|
||||
store.read_with(cx, |store, _| {
|
||||
store
|
||||
.metadata_cache
|
||||
.read()
|
||||
.metadata_by_id
|
||||
.contains_key(&commit_message_id)
|
||||
}),
|
||||
"Built-in prompt should always be in cache"
|
||||
);
|
||||
|
||||
let custom_content = "Custom commit message prompt";
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(custom_content),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let loaded_custom = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
loaded_custom.trim(),
|
||||
custom_content.trim(),
|
||||
"Custom content should be loaded after saving"
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.read_with(cx, |store, _| store.metadata(commit_message_id))
|
||||
.is_some(),
|
||||
"Built-in prompt should have metadata after customization"
|
||||
);
|
||||
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(BuiltInPrompt::CommitMessage.default_content()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata_after_reset =
|
||||
store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata_after_reset.is_some(),
|
||||
"Built-in prompt should still have metadata after reset"
|
||||
);
|
||||
assert_eq!(
|
||||
metadata_after_reset
|
||||
.as_ref()
|
||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
||||
Some("Commit message"),
|
||||
"Built-in prompt should have default title after reset"
|
||||
);
|
||||
|
||||
let loaded_after_reset = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut expected_content_after_reset =
|
||||
BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content_after_reset);
|
||||
assert_eq!(
|
||||
loaded_after_reset.trim(),
|
||||
expected_content_after_reset.trim(),
|
||||
"After saving default content, load should return default"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
|
||||
use editor::{CompletionProvider, SelectionEffects};
|
||||
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
|
||||
use gpui::{
|
||||
Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable,
|
||||
PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle,
|
||||
WindowOptions, actions, point, size, transparent_black,
|
||||
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
|
||||
Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, point, size, transparent_black,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
|
||||
use language_model::{
|
||||
@@ -21,7 +21,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use title_bar::platform_title_bar::PlatformTitleBar;
|
||||
use ui::{Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
|
||||
use zed_actions::assistant::InlineAssist;
|
||||
@@ -206,13 +206,8 @@ impl PickerDelegate for RulePickerDelegate {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.read(cx).prompt_count() == 0 {
|
||||
"No rules.".into()
|
||||
} else {
|
||||
"No rules found matching your search.".into()
|
||||
};
|
||||
Some(text)
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("No rules found matching your search.".into())
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -680,13 +675,13 @@ impl RulesLibrary {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(default_content) = prompt_id.default_content() else {
|
||||
let Some(built_in) = prompt_id.as_built_in() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
|
||||
rule_editor.body_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(default_content, window, cx);
|
||||
editor.set_text(built_in.default_content(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1428,31 +1423,7 @@ impl Render for RulesLibrary {
|
||||
this.border_t_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(self.render_rule_list(cx))
|
||||
.map(|el| {
|
||||
if self.store.read(cx).prompt_count() == 0 {
|
||||
el.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
Button::new("create-rule", "New Rule")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.key_binding(KeyBinding::for_action(&NewRule, cx))
|
||||
.on_click(|_, window, cx| {
|
||||
window
|
||||
.dispatch_action(NewRule.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
el.child(self.render_active_rule(cx))
|
||||
}
|
||||
}),
|
||||
.child(self.render_active_rule(cx)),
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -288,6 +288,11 @@ impl std::fmt::Debug for ContextServerCommand {
|
||||
#[with_fallible_options]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not to enable git integration.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(flatten)]
|
||||
pub enabled: Option<GitEnabledSettings>,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -317,6 +322,25 @@ pub struct GitSettings {
|
||||
pub path_style: Option<GitPathStyle>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GitEnabledSettings {
|
||||
pub disable_git: Option<bool>,
|
||||
pub enable_status: Option<bool>,
|
||||
pub enable_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl GitEnabledSettings {
|
||||
pub fn is_git_status_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_status.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn is_git_diff_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_diff.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
||||
@@ -5519,6 +5519,102 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
SettingsPage {
|
||||
title: "Version Control",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("Git Integration"),
|
||||
SettingsPageItem::DynamicItem(DynamicItem {
|
||||
discriminant: SettingItem {
|
||||
files: USER,
|
||||
title: "Disable Git Integration",
|
||||
description: "Disable all Git integration features in Zed.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.disable_git"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.disable_git = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
pick_discriminant: |settings_content| {
|
||||
let disabled = settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.unwrap_or(false);
|
||||
Some(if disabled { 0 } else { 1 })
|
||||
},
|
||||
fields: vec![
|
||||
vec![],
|
||||
vec![
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Status",
|
||||
description: "Show Git status information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_status"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_status
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_status = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Diff",
|
||||
description: "Show Git diff information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_diff"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_diff
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_diff = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Git Gutter"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Visibility",
|
||||
|
||||
@@ -893,39 +893,57 @@ impl ContextMenu {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
documentation_aside,
|
||||
..
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.entity().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
let keep_open_on_confirm = self.keep_open_on_confirm;
|
||||
move |_, window, cx| {
|
||||
handler(context.as_ref(), window, cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
|
||||
if keep_open_on_confirm {
|
||||
menu.rebuild(window, cx);
|
||||
} else {
|
||||
cx.emit(DismissEvent);
|
||||
div()
|
||||
.id(("context-menu-child", ix))
|
||||
.when_some(documentation_aside.clone(), |this, documentation_aside| {
|
||||
this.occlude()
|
||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.documentation_aside = Some((ix, documentation_aside.clone()));
|
||||
} else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
|
||||
{
|
||||
menu.documentation_aside = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
let keep_open_on_confirm = self.keep_open_on_confirm;
|
||||
move |_, window, cx| {
|
||||
handler(context.as_ref(), window, cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
|
||||
if keep_open_on_confirm {
|
||||
menu.rebuild(window, cx);
|
||||
} else {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(window, cx))
|
||||
})
|
||||
.child(entry_render(window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,17 +126,6 @@ enum IconSource {
|
||||
ExternalSvg(SharedString),
|
||||
}
|
||||
|
||||
impl IconSource {
|
||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
if path.starts_with("icons/") {
|
||||
Self::Embedded(path)
|
||||
} else {
|
||||
Self::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct Icon {
|
||||
source: IconSource,
|
||||
@@ -155,9 +144,18 @@ impl Icon {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
|
||||
/// - Paths starting with "icons/" are treated as embedded SVGs
|
||||
/// - Other paths are treated as external raster images (from icon themes)
|
||||
pub fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
let source = if path.starts_with("icons/") {
|
||||
IconSource::Embedded(path)
|
||||
} else {
|
||||
IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
};
|
||||
Self {
|
||||
source: IconSource::from_path(path),
|
||||
source,
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
|
||||
});
|
||||
|
||||
/// Whether a given environment variable name should have its value redacted
|
||||
pub fn should_redact(env_var_name: &str) -> bool {
|
||||
const REDACTED_SUFFIXES: &[&str] = &[
|
||||
@@ -13,3 +19,31 @@ pub fn should_redact(env_var_name: &str) -> bool {
|
||||
.iter()
|
||||
.any(|suffix| env_var_name.ends_with(suffix))
|
||||
}
|
||||
|
||||
/// Redact a string which could include a command with environment variables
|
||||
pub fn redact_command(command: &str) -> String {
|
||||
REDACT_REGEX
|
||||
.replace_all(command, |caps: ®ex::Captures| {
|
||||
let var_name = &caps[1];
|
||||
let value = &caps[2];
|
||||
if should_redact(var_name) {
|
||||
format!(r#"{}="[REDACTED]""#, var_name)
|
||||
} else {
|
||||
format!("{}={}", var_name, value)
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_redact_string_with_multiple_env_vars() {
|
||||
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
|
||||
let result = redact_command(input);
|
||||
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,13 @@ impl Settings for ItemSettings {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let tabs = content.tabs.as_ref().unwrap();
|
||||
Self {
|
||||
git_status: tabs.git_status.unwrap(),
|
||||
git_status: tabs.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
close_position: tabs.close_position.unwrap(),
|
||||
activate_on_close: tabs.activate_on_close.unwrap(),
|
||||
file_icons: tabs.file_icons.unwrap(),
|
||||
|
||||
@@ -564,6 +564,11 @@ pub fn main() {
|
||||
dap_adapters::init(cx);
|
||||
auto_update_ui::init(cx);
|
||||
reliability::init(client.clone(), cx);
|
||||
// Initialize the language model registry first, then set up the extension proxy
|
||||
// BEFORE extension_host::init so that extensions can register their LLM providers
|
||||
// when they load.
|
||||
language_model::init(app_state.client.clone(), cx);
|
||||
language_models::init_extension_proxy(cx);
|
||||
extension_host::init(
|
||||
extension_host_proxy.clone(),
|
||||
app_state.fs.clone(),
|
||||
@@ -589,7 +594,6 @@ pub fn main() {
|
||||
cx,
|
||||
);
|
||||
supermaven::init(app_state.client.clone(), cx);
|
||||
language_model::init(app_state.client.clone(), cx);
|
||||
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
||||
acp_tools::init(cx);
|
||||
edit_prediction_ui::init(cx);
|
||||
|
||||
@@ -177,7 +177,6 @@
|
||||
- [Linux](./development/linux.md)
|
||||
- [Windows](./development/windows.md)
|
||||
- [FreeBSD](./development/freebsd.md)
|
||||
- [Local Collaboration](./development/local-collaboration.md)
|
||||
- [Using Debuggers](./development/debuggers.md)
|
||||
- [Performance](./performance.md)
|
||||
- [Glossary](./development/glossary.md)
|
||||
|
||||
@@ -6,10 +6,6 @@ See the platform-specific instructions for building Zed from source:
|
||||
- [Linux](./development/linux.md)
|
||||
- [Windows](./development/windows.md)
|
||||
|
||||
If you'd like to develop collaboration features, additionally see:
|
||||
|
||||
- [Local Collaboration](./development/local-collaboration.md)
|
||||
|
||||
## Keychain access
|
||||
|
||||
Zed stores secrets in the system keychain.
|
||||
|
||||
@@ -73,7 +73,7 @@ h_flex()
|
||||
|
||||
- `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering.
|
||||
- `Modal`: A UI element that floats on top of the rest of the UI
|
||||
- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.)
|
||||
- `Picker`: A struct representing a list of items floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Modal' in the image below is a picker.)
|
||||
- `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate.
|
||||
- `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below).
|
||||
- `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below).
|
||||
|
||||
@@ -16,10 +16,6 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed).
|
||||
|
||||
If you prefer to install the system libraries manually, you can find the list of required packages in the `script/linux` file.
|
||||
|
||||
### Backend Dependencies (optional) {#backend-dependencies}
|
||||
|
||||
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
|
||||
|
||||
### Linkers {#linker}
|
||||
|
||||
On Linux, Rust's default linker is [LLVM's `lld`](https://blog.rust-lang.org/2025/09/18/Rust-1.90.0/). Alternative linkers, especially [Wild](https://github.com/davidlattimore/wild) and [Mold](https://github.com/rui314/mold) can significantly improve clean and incremental build time.
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
# Local Collaboration
|
||||
|
||||
1. Ensure you have access to our cloud infrastructure. If you don't have access, you can't collaborate locally at this time.
|
||||
|
||||
2. Make sure you've installed Zed's dependencies for your platform:
|
||||
|
||||
- [macOS](#macos)
|
||||
- [Linux](#linux)
|
||||
- [Windows](#backend-windows)
|
||||
|
||||
Note that `collab` can be compiled only with MSVC toolchain on Windows
|
||||
|
||||
3. Clone down our cloud repository and follow the instructions in the cloud README
|
||||
|
||||
4. Setup the local database for your platform:
|
||||
|
||||
- [macOS & Linux](#database-unix)
|
||||
- [Windows](#database-windows)
|
||||
|
||||
5. Run collab:
|
||||
|
||||
- [macOS & Linux](#run-collab-unix)
|
||||
- [Windows](#run-collab-windows)
|
||||
|
||||
## Backend Dependencies
|
||||
|
||||
If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server:
|
||||
|
||||
- PostgreSQL
|
||||
- LiveKit
|
||||
- Foreman
|
||||
|
||||
You can install these dependencies natively or run them under Docker.
|
||||
|
||||
### macOS
|
||||
|
||||
1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15):
|
||||
|
||||
```sh
|
||||
brew install postgresql@15
|
||||
```
|
||||
|
||||
2. Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman)
|
||||
|
||||
```sh
|
||||
brew install livekit foreman
|
||||
```
|
||||
|
||||
- Follow the steps in the [collab README](https://github.com/zed-industries/zed/blob/main/crates/collab/README.md) to configure the Postgres database for integration tests
|
||||
|
||||
Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Install [Postgres](https://www.postgresql.org/download/linux/)
|
||||
|
||||
```sh
|
||||
sudo apt-get install postgresql # Ubuntu/Debian
|
||||
sudo pacman -S postgresql # Arch Linux
|
||||
sudo dnf install postgresql postgresql-server # RHEL/Fedora
|
||||
sudo zypper install postgresql postgresql-server # OpenSUSE
|
||||
```
|
||||
|
||||
2. Install [Livekit](https://github.com/livekit/livekit-cli)
|
||||
|
||||
```sh
|
||||
curl -sSL https://get.livekit.io/cli | bash
|
||||
```
|
||||
|
||||
3. Install [Foreman](https://theforeman.org/manuals/3.15/quickstart_guide.html)
|
||||
|
||||
### Windows {#backend-windows}
|
||||
|
||||
> This section is still in development. The instructions are not yet complete.
|
||||
|
||||
- Install [Postgres](https://www.postgresql.org/download/windows/)
|
||||
- Install [Livekit](https://github.com/livekit/livekit), optionally you can add the `livekit-server` binary to your `PATH`.
|
||||
|
||||
Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose.
|
||||
|
||||
### Docker {#Docker}
|
||||
|
||||
If you have docker or podman available, you can run the backend dependencies inside containers with Docker Compose:
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Database setup
|
||||
|
||||
Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database.
|
||||
|
||||
### On macOS and Linux {#database-unix}
|
||||
|
||||
```sh
|
||||
script/bootstrap
|
||||
```
|
||||
|
||||
This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API.
|
||||
|
||||
The script will seed the database with various content defined by:
|
||||
|
||||
```sh
|
||||
cat crates/collab/seed.default.json
|
||||
```
|
||||
|
||||
To use a different set of admin users, you can create your own version of that json file and export the `SEED_PATH` environment variable. Note that the usernames listed in the admins list currently must correspond to valid GitHub users.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"admins": ["admin1", "admin2"],
|
||||
"channels": ["zed"]
|
||||
}
|
||||
```
|
||||
|
||||
### On Windows {#database-windows}
|
||||
|
||||
```powershell
|
||||
.\script\bootstrap.ps1
|
||||
```
|
||||
|
||||
## Testing collaborative features locally
|
||||
|
||||
### On macOS and Linux {#run-collab-unix}
|
||||
|
||||
Ensure that Postgres is configured and running, then run Zed's collaboration server and the `livekit` dev server:
|
||||
|
||||
```sh
|
||||
foreman start
|
||||
# OR
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Alternatively, if you're not testing voice and screenshare, you can just run `collab` and `cloud`, and not the `livekit` dev server:
|
||||
|
||||
```sh
|
||||
cargo run -p collab -- serve all
|
||||
```
|
||||
|
||||
```sh
|
||||
cd ../cloud; cargo make dev
|
||||
```
|
||||
|
||||
In a new terminal, run two or more instances of Zed.
|
||||
|
||||
```sh
|
||||
script/zed-local -3
|
||||
```
|
||||
|
||||
This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `.admins.json` or `.admins.default.json`.
|
||||
|
||||
### On Windows {#run-collab-windows}
|
||||
|
||||
Since `foreman` is not available on Windows, you can run the following commands in separate terminals:
|
||||
|
||||
```powershell
|
||||
cargo run --package=collab -- serve all
|
||||
```
|
||||
|
||||
If you have added the `livekit-server` binary to your `PATH`, you can run:
|
||||
|
||||
```powershell
|
||||
livekit-server --dev
|
||||
```
|
||||
|
||||
Otherwise,
|
||||
|
||||
```powershell
|
||||
.\path\to\livekit-serve.exe --dev
|
||||
```
|
||||
|
||||
You'll also need to start the cloud server:
|
||||
|
||||
```powershell
|
||||
cd ..\cloud; cargo make dev
|
||||
```
|
||||
|
||||
In a new terminal, run two or more instances of Zed.
|
||||
|
||||
```powershell
|
||||
node .\script\zed-local -2
|
||||
```
|
||||
|
||||
Note that this requires `node.exe` to be in your `PATH`.
|
||||
|
||||
## Running a local collab server
|
||||
|
||||
> [!NOTE]
|
||||
> Because of recent changes to our authentication system, Zed will not be able to authenticate itself with, and therefore use, a local collab server.
|
||||
|
||||
If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no support for authentication nor extensions.
|
||||
|
||||
Configuration is done through environment variables. By default it will read the configuration from [`.env.toml`](https://github.com/zed-industries/zed/blob/main/crates/collab/.env.toml) and you should use that as a guide for setting this up.
|
||||
|
||||
By default Zed assumes that the DATABASE_URL is a Postgres database, but you can make it use Sqlite by compiling with `--features sqlite` and using a sqlite DATABASE_URL with `?mode=rwc`.
|
||||
|
||||
To authenticate you must first configure the server by creating a seed.json file that contains at a minimum your github handle. This will be used to create the user on demand.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"admins": ["nathansobo"]
|
||||
}
|
||||
```
|
||||
|
||||
By default the collab server will seed the database when first creating it, but if you want to add more users you can explicitly reseed them with `SEED_PATH=./seed.json cargo run -p collab seed`
|
||||
|
||||
Then when running the zed client you must specify two environment variables, `ZED_ADMIN_API_TOKEN` (which should match the value of `API_TOKEN` in .env.toml) and `ZED_IMPERSONATE` (which should match one of the users in your seed.json)
|
||||
@@ -31,10 +31,6 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed).
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
### Backend Dependencies (optional) {#backend-dependencies}
|
||||
|
||||
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
|
||||
|
||||
## Building Zed from Source
|
||||
|
||||
Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/).
|
||||
|
||||
@@ -66,10 +66,6 @@ The list can be obtained as follows:
|
||||
- Click on `More` in the `Installed` tab
|
||||
- Click on `Export configuration`
|
||||
|
||||
### Backend Dependencies (optional) {#backend-dependencies}
|
||||
|
||||
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
|
||||
|
||||
### Notes
|
||||
|
||||
You should modify the `pg_hba.conf` file in the `data` directory to use `trust` instead of `scram-sha-256` for the `host` method. Otherwise, the connection will fail with the error `password authentication failed`. The `pg_hba.conf` file typically locates at `C:\Program Files\PostgreSQL\17\data\pg_hba.conf`. After the modification, the file should look like this:
|
||||
|
||||
@@ -6,6 +6,9 @@ prHygiene({
|
||||
rules: {
|
||||
// Don't enable this rule just yet, as it can have false positives.
|
||||
useImperativeMood: "off",
|
||||
noConventionalCommits: {
|
||||
bannedTypes: ["feat", "fix", "style", "refactor", "perf", "test", "chore", "build", "revert"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"danger": "13.0.4",
|
||||
"danger-plugin-pr-hygiene": "0.6.1"
|
||||
"danger-plugin-pr-hygiene": "0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
10
script/danger/pnpm-lock.yaml
generated
10
script/danger/pnpm-lock.yaml
generated
@@ -12,8 +12,8 @@ importers:
|
||||
specifier: 13.0.4
|
||||
version: 13.0.4
|
||||
danger-plugin-pr-hygiene:
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -134,8 +134,8 @@ packages:
|
||||
core-js@3.45.1:
|
||||
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
|
||||
|
||||
danger-plugin-pr-hygiene@0.6.1:
|
||||
resolution: {integrity: sha512-nb+iUQvirE3BlKXI1WoOND6sujyGzHar590mJm5tt4RLi65HXFaU5hqONxgDoWFujJNHYnXse9yaZdxnxEi4QA==}
|
||||
danger-plugin-pr-hygiene@0.7.1:
|
||||
resolution: {integrity: sha512-ll070nNaL3OeO2nooYWflPE/CRKLeq8GiH2C68u5zM3gW4gepH89GhVv0sYNNGLx4cYwa1zZ/TuiYYhC49z06Q==}
|
||||
|
||||
danger@13.0.4:
|
||||
resolution: {integrity: sha512-IAdQ5nSJyIs4zKj6AN35ixt2B0Ce3WZUm3IFe/CMnL/Op7wV7IGg4D348U0EKNaNPP58QgXbdSk9pM+IXP1QXg==}
|
||||
@@ -573,7 +573,7 @@ snapshots:
|
||||
|
||||
core-js@3.45.1: {}
|
||||
|
||||
danger-plugin-pr-hygiene@0.6.1: {}
|
||||
danger-plugin-pr-hygiene@0.7.1: {}
|
||||
|
||||
danger@13.0.4:
|
||||
dependencies:
|
||||
|
||||
@@ -45,15 +45,11 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
&should_run_tests,
|
||||
]);
|
||||
|
||||
let check_style = check_style();
|
||||
let run_tests_linux = run_platform_tests(Platform::Linux);
|
||||
let call_autofix = call_autofix(&check_style, &run_tests_linux);
|
||||
|
||||
let mut jobs = vec![
|
||||
orchestrate,
|
||||
check_style,
|
||||
check_style(),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Windows)),
|
||||
should_run_tests.guard(run_tests_linux),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Linux)),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
||||
should_run_tests.guard(doctests()),
|
||||
should_run_tests.guard(check_workspace_binaries()),
|
||||
@@ -110,7 +106,6 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
workflow
|
||||
})
|
||||
.add_job(tests_pass.name, tests_pass.job)
|
||||
.add_job(call_autofix.name, call_autofix.job)
|
||||
}
|
||||
|
||||
// Generates a bash script that checks changed files against regex patterns
|
||||
@@ -226,8 +221,6 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
|
||||
|
||||
fn check_style() -> NamedJob {
|
||||
fn check_for_typos() -> Step<Use> {
|
||||
named::uses(
|
||||
@@ -245,56 +238,12 @@ fn check_style() -> NamedJob {
|
||||
.add_step(steps::setup_pnpm())
|
||||
.add_step(steps::prettier())
|
||||
.add_step(steps::cargo_fmt())
|
||||
.add_step(steps::record_style_failure())
|
||||
.add_step(steps::script("./script/check-todos"))
|
||||
.add_step(steps::script("./script/check-keymaps"))
|
||||
.add_step(check_for_typos())
|
||||
.outputs([(
|
||||
STYLE_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_STYLE_FAILURE_STEP_ID
|
||||
),
|
||||
)]),
|
||||
.add_step(check_for_typos()),
|
||||
)
|
||||
}
|
||||
|
||||
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
|
||||
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
|
||||
let clippy_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
|
||||
);
|
||||
named::bash(format!(
|
||||
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
|
||||
clippy_failed_expr
|
||||
))
|
||||
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
|
||||
}
|
||||
|
||||
let style_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
check_style.name, STYLE_FAILED_OUTPUT
|
||||
);
|
||||
let clippy_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
|
||||
);
|
||||
let (authenticate, _token) = steps::authenticate_as_zippy();
|
||||
|
||||
let job = Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(format!(
|
||||
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
|
||||
style_failed_expr, clippy_failed_expr
|
||||
)))
|
||||
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
|
||||
.add_step(authenticate)
|
||||
.add_step(dispatch_autofix(&run_tests_linux.name));
|
||||
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
fn check_dependencies() -> NamedJob {
|
||||
fn install_cargo_machete() -> Step<Use> {
|
||||
named::uses(
|
||||
@@ -355,8 +304,6 @@ fn check_workspace_binaries() -> NamedJob {
|
||||
)
|
||||
}
|
||||
|
||||
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
|
||||
|
||||
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
||||
let runner = match platform {
|
||||
Platform::Windows => runners::WINDOWS_DEFAULT,
|
||||
@@ -378,24 +325,12 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
||||
)
|
||||
.add_step(steps::setup_node())
|
||||
.add_step(steps::clippy(platform))
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.add_step(steps::record_clippy_failure())
|
||||
})
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.add_step(steps::cargo_install_nextest())
|
||||
})
|
||||
.add_step(steps::clear_target_dir_if_large(platform))
|
||||
.add_step(steps::cargo_nextest(platform))
|
||||
.add_step(steps::cleanup_cargo_config(platform))
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.outputs([(
|
||||
CLIPPY_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_CLIPPY_FAILURE_STEP_ID
|
||||
),
|
||||
)])
|
||||
}),
|
||||
.add_step(steps::cleanup_cargo_config(platform)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,25 +54,12 @@ pub fn setup_sentry() -> Step<Use> {
|
||||
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
|
||||
}
|
||||
|
||||
pub const PRETTIER_STEP_ID: &str = "prettier";
|
||||
pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
|
||||
pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
|
||||
|
||||
pub fn prettier() -> Step<Run> {
|
||||
named::bash("./script/prettier").id(PRETTIER_STEP_ID)
|
||||
named::bash("./script/prettier")
|
||||
}
|
||||
|
||||
pub fn cargo_fmt() -> Step<Run> {
|
||||
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
|
||||
}
|
||||
|
||||
pub fn record_style_failure() -> Step<Run> {
|
||||
named::bash(format!(
|
||||
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
|
||||
))
|
||||
.id(RECORD_STYLE_FAILURE_STEP_ID)
|
||||
.if_condition(Expression::new("always()"))
|
||||
named::bash("cargo fmt --all -- --check")
|
||||
}
|
||||
|
||||
pub fn cargo_install_nextest() -> Step<Use> {
|
||||
@@ -118,25 +105,13 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
|
||||
}
|
||||
}
|
||||
|
||||
pub const CLIPPY_STEP_ID: &str = "clippy";
|
||||
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
|
||||
|
||||
pub fn clippy(platform: Platform) -> Step<Run> {
|
||||
match platform {
|
||||
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
|
||||
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
|
||||
Platform::Windows => named::pwsh("./script/clippy.ps1"),
|
||||
_ => named::bash("./script/clippy"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_clippy_failure() -> Step<Run> {
|
||||
named::bash(format!(
|
||||
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||
CLIPPY_STEP_ID
|
||||
))
|
||||
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
|
||||
.if_condition(Expression::new("always()"))
|
||||
}
|
||||
|
||||
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
|
||||
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user