Detailed changes
@@ -19,6 +19,18 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
+ - name: Install mold linker
+ shell: bash -euxo pipefail {0}
+ run: ./script/install-mold
+
+ - name: Download WASI SDK
+ shell: bash -euxo pipefail {0}
+ run: ./script/download-wasi-sdk
+
+ - name: Generate action metadata
+ shell: bash -euxo pipefail {0}
+ run: ./script/generate-action-metadata
+
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -372,6 +353,9 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
+ - name: ./script/generate-action-metadata
+ run: ./script/generate-action-metadata
+ shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:
@@ -592,24 +576,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
@@ -36,6 +36,7 @@
DerivedData/
Packages
xcuserdata/
+crates/docs_preprocessor/actions.json
# Don't commit any secrets to the repo.
.env
@@ -5021,8 +5021,6 @@ name = "docs_preprocessor"
version = "0.1.0"
dependencies = [
"anyhow",
- "command_palette",
- "gpui",
"mdbook",
"regex",
"serde",
@@ -5031,7 +5029,6 @@ dependencies = [
"task",
"theme",
"util",
- "zed",
"zlog",
]
@@ -8932,6 +8929,8 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
+ "extension",
+ "extension_host",
"fs",
"futures 0.3.31",
"google_ai",
@@ -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
@@ -210,12 +210,21 @@ pub trait AgentModelSelector: 'static {
}
}
+/// Icon for a model in the model selector.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum AgentModelIcon {
+ /// A built-in icon from Zed's icon set.
+ Named(IconName),
+ /// Path to a custom SVG icon file.
+ Path(SharedString),
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentModelInfo {
pub id: acp::ModelId,
pub name: SharedString,
pub description: Option<SharedString>,
- pub icon: Option<IconName>,
+ pub icon: Option<AgentModelIcon>,
}
impl From<acp::ModelInfo> for AgentModelInfo {
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
-use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
+use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
@@ -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<_>>();
@@ -153,7 +153,10 @@ impl LanguageModels {
id: Self::model_id(model),
name: model.name().0,
description: None,
- icon: Some(provider.icon()),
+ icon: Some(match provider.icon() {
+ IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
+ IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
+ }),
}
}
@@ -164,7 +167,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<_>>();
@@ -1630,7 +1633,9 @@ mod internal_tests {
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
- icon: Some(ui::IconName::ZedAssistant),
+ icon: Some(acp_thread::AgentModelIcon::Named(
+ ui::IconName::ZedAssistant
+ )),
}]
)])
);
@@ -188,25 +188,25 @@ impl Render for ModeSelector {
.gap_1()
.child(
h_flex()
- .pb_1()
.gap_2()
.justify_between()
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
- .child(Label::new("Cycle Through Modes"))
+ .child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
- &CycleModeSelector,
+ &ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.child(
h_flex()
+ .pb_1()
.gap_2()
.justify_between()
- .child(Label::new("Toggle Mode Menu"))
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(Label::new("Cycle Through Modes"))
.child(KeyBinding::for_action_in(
- &ToggleProfileSelector,
+ &CycleModeSelector,
&focus_handle,
cx,
)),
@@ -1,6 +1,6 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
-use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
+use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol::ModelId;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
@@ -350,7 +350,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(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
+ Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
+ None => this,
+ })
.is_selected(is_selected)
.is_focused(selected)
.when(supports_favorites, |this| {
@@ -1,7 +1,7 @@
use std::rc::Rc;
use std::sync::Arc;
-use acp_thread::{AgentModelInfo, AgentModelSelector};
+use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
@@ -70,7 +70,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 +125,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 {
+ AgentModelIcon::Path(path) => Icon::from_external_svg(path),
+ AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
+ }
+ .color(color)
+ .size(IconSize::XSmall),
+ )
})
.child(
Label::new(model_name)
@@ -2718,7 +2718,7 @@ impl AcpThreadView {
..default_markdown_style(false, true, window, cx)
},
))
- .tooltip(Tooltip::text("Jump to File"))
+ .tooltip(Tooltip::text("Go to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, 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);
}
_ => {}
@@ -7,8 +7,8 @@ use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, 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 +55,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();
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
- icon: IconName,
+ icon: IconOrSvg,
is_favorite: bool,
}
@@ -203,7 +203,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 +474,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 +566,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 +705,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,
}
})
@@ -191,6 +191,9 @@ impl Render for ProfileSelector {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
+ .child(container().child(Label::new("Toggle Profile Menu")).child(
+ KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
+ ))
.child(
container()
.pb_1()
@@ -203,9 +206,6 @@ impl Render for ProfileSelector {
cx,
)),
)
- .child(container().child(Label::new("Toggle Profile Menu")).child(
- KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
- ))
.into_any()
}
}),
@@ -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,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())
@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
-command_palette.workspace = true
-gpui.workspace = true
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
# Ask @maxdeviant about this before bumping.
mdbook = "= 0.4.40"
@@ -17,7 +15,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
util.workspace = true
-zed.workspace = true
zlog.workspace = true
task.workspace = true
theme.workspace = true
@@ -27,4 +24,4 @@ workspace = true
[[bin]]
name = "docs_preprocessor"
-path = "src/main.rs"
+path = "src/main.rs"
@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
-static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
+static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
fn main() -> Result<()> {
zlog::init();
zlog::init_output_stderr();
- // call a zed:: function so everything in `zed` crate is linked and
- // all actions in the actual app are registered
- zed::stdout_is_a_pty();
let args = std::env::args().skip(1).collect::<Vec<_>>();
match args.get(0).map(String::as_str) {
@@ -72,8 +69,8 @@ enum PreprocessorError {
impl PreprocessorError {
fn new_for_not_found_action(action_name: String) -> Self {
for action in &*ALL_ACTIONS {
- for alias in action.deprecated_aliases {
- if alias == &action_name {
+ for alias in &action.deprecated_aliases {
+ if alias == action_name.as_str() {
return PreprocessorError::DeprecatedActionUsed {
used: action_name,
should_be: action.name.to_string(),
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
chapter.content = regex
.replace_all(&chapter.content, |caps: ®ex::Captures| {
let action = caps[1].trim();
- if find_action_by_name(action).is_none() {
+ if is_missing_action(action) {
errors.insert(PreprocessorError::new_for_not_found_action(
action.to_string(),
));
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
.replace_all(&chapter.content, |caps: ®ex::Captures| {
let name = caps[1].trim();
let Some(action) = find_action_by_name(name) else {
- errors.insert(PreprocessorError::new_for_not_found_action(
- name.to_string(),
- ));
- return String::new();
+ if actions_available() {
+ errors.insert(PreprocessorError::new_for_not_found_action(
+ name.to_string(),
+ ));
+ }
+ return format!("<code class=\"hljs\">{}</code>", name);
};
format!("<code class=\"hljs\">{}</code>", &action.human_name)
})
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
ALL_ACTIONS
- .binary_search_by(|action| action.name.cmp(name))
+ .binary_search_by(|action| action.name.as_str().cmp(name))
.ok()
.map(|index| &ALL_ACTIONS[index])
}
+fn actions_available() -> bool {
+ !ALL_ACTIONS.is_empty()
+}
+
+fn is_missing_action(name: &str) -> bool {
+ actions_available() && find_action_by_name(name).is_none()
+}
+
fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
.context("Failed to parse keymap JSON")?;
for section in keymap.sections() {
- for (keystrokes, action) in section.bindings() {
- keystrokes
- .split_whitespace()
- .map(|source| gpui::Keystroke::parse(source))
- .collect::<std::result::Result<Vec<_>, _>>()
- .context("Failed to parse keystroke")?;
+ for (_keystrokes, action) in section.bindings() {
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
.map_err(|err| anyhow::format_err!(err))
.context("Failed to parse action")?
{
anyhow::ensure!(
- find_action_by_name(action_name).is_some(),
+ !is_missing_action(action_name),
"Action not found: {}",
action_name
);
@@ -491,27 +493,35 @@ where
});
}
-#[derive(Debug, serde::Serialize)]
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct ActionDef {
- name: &'static str,
+ name: String,
human_name: String,
- deprecated_aliases: &'static [&'static str],
- docs: Option<&'static str>,
+ deprecated_aliases: Vec<String>,
+ #[serde(rename = "documentation")]
+ docs: Option<String>,
}
-fn dump_all_gpui_actions() -> Vec<ActionDef> {
- let mut actions = gpui::generate_list_of_all_registered_actions()
- .map(|action| ActionDef {
- name: action.name,
- human_name: command_palette::humanize_action_name(action.name),
- deprecated_aliases: action.deprecated_aliases,
- docs: action.documentation,
- })
- .collect::<Vec<ActionDef>>();
-
- actions.sort_by_key(|a| a.name);
-
- actions
+fn load_all_actions() -> Vec<ActionDef> {
+ let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
+ match std::fs::read_to_string(asset_path) {
+ Ok(content) => {
+ let mut actions: Vec<ActionDef> =
+ serde_json::from_str(&content).expect("Failed to parse actions.json");
+ actions.sort_by(|a, b| a.name.cmp(&b.name));
+ actions
+ }
+ Err(err) => {
+ if std::env::var("CI").is_ok() {
+ panic!("actions.json not found at {}: {}", asset_path, err);
+ }
+ eprintln!(
+ "Warning: actions.json not found, action validation will be skipped: {}",
+ err
+ );
+ Vec::new()
+ }
+ }
}
fn handle_postprocessing() -> Result<()> {
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
let mut output = String::new();
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
- actions_sorted.sort_by_key(|a| a.name);
+ actions_sorted.sort_by_key(|a| a.name.as_str());
// Start the definition list with custom styling for better spacing
output.push_str("<dl style=\"line-height: 1.8;\">\n");
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
// Add the description, escaping HTML if needed
- if let Some(description) = action.docs {
+ if let Some(description) = action.docs.as_ref() {
output.push_str(
&description
.replace("&", "&")
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<br>\n");
}
output.push_str("Keymap Name: <code>");
- output.push_str(action.name);
+ output.push_str(&action.name);
output.push_str("</code><br>\n");
if !action.deprecated_aliases.is_empty() {
output.push_str("Deprecated Alias(es): ");
@@ -205,6 +205,49 @@ impl EditorLspTestContext {
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
"#})),
+ text_objects: Some(Cow::from(indoc! {r#"
+ (function_declaration
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ (method_definition
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ ; Arrow function in variable declaration - capture the full declaration
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ ]) @function.around
+
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ ]) @function.around
+
+ ; Catch-all for arrow functions in other contexts (callbacks, etc.)
+ ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -276,6 +319,49 @@ impl EditorLspTestContext {
(jsx_opening_element) @start
(jsx_closing_element)? @end) @indent
"#})),
+ text_objects: Some(Cow::from(indoc! {r#"
+ (function_declaration
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ (method_definition
+ body: (_
+ "{"
+ (_)* @function.inside
+ "}")) @function.around
+
+ ; Arrow function in variable declaration - capture the full declaration
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ ]) @function.around
+
+ ([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function)))
+ ]) @function.around
+
+ ; Catch-all for arrow functions in other contexts (callbacks, etc.)
+ ((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
+ "#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -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(),
}
}
@@ -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,
},
@@ -0,0 +1,155 @@
+use gpui::{App, Context, WeakEntity, Window};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use std::sync::Arc;
+use ui::{Color, IconName, SharedString};
+use util::ResultExt;
+use workspace::{self, Workspace};
+
+pub fn clone_and_open(
+ repo_url: SharedString,
+ workspace: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+ on_success: Arc<
+ dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
+ >,
+) {
+ let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
+ files: false,
+ directories: true,
+ multiple: false,
+ prompt: Some("Select as Repository Destination".into()),
+ });
+
+ window
+ .spawn(cx, async move |cx| {
+ let mut paths = destination_prompt.await.ok()?.ok()??;
+ let mut destination_dir = paths.pop()?;
+
+ let repo_name = repo_url
+ .split('/')
+ .next_back()
+ .map(|name| name.strip_suffix(".git").unwrap_or(name))
+ .unwrap_or("repository")
+ .to_owned();
+
+ let clone_task = workspace
+ .update(cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ let destination_dir = destination_dir.clone();
+ let repo_url = repo_url.clone();
+ cx.spawn(async move |_workspace, _cx| {
+ fs.git_clone(&repo_url, destination_dir.as_path()).await
+ })
+ })
+ .ok()?;
+
+ if let Err(error) = clone_task.await {
+ workspace
+ .update(cx, |workspace, cx| {
+ let toast = StatusToast::new(error.to_string(), cx, |this, _| {
+ this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
+ .dismiss_button(true)
+ });
+ workspace.toggle_status_toast(toast, cx);
+ })
+ .log_err();
+ return None;
+ }
+
+ let has_worktrees = workspace
+ .read_with(cx, |workspace, cx| {
+ workspace.project().read(cx).worktrees(cx).next().is_some()
+ })
+ .ok()?;
+
+ let prompt_answer = if has_worktrees {
+ cx.update(|window, cx| {
+ window.prompt(
+ gpui::PromptLevel::Info,
+ &format!("Git Clone: {}", repo_name),
+ None,
+ &["Add repo to project", "Open repo in new project"],
+ cx,
+ )
+ })
+ .ok()?
+ .await
+ .ok()?
+ } else {
+ // Don't ask if project is empty
+ 0
+ };
+
+ destination_dir.push(&repo_name);
+
+ match prompt_answer {
+ 0 => {
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ let create_task = workspace.project().update(cx, |project, cx| {
+ project.create_worktree(destination_dir.as_path(), true, cx)
+ });
+
+ let workspace_weak = cx.weak_entity();
+ let on_success = on_success.clone();
+ cx.spawn_in(window, async move |_window, cx| {
+ if create_task.await.log_err().is_some() {
+ workspace_weak
+ .update_in(cx, |workspace, window, cx| {
+ (on_success)(workspace, window, cx);
+ })
+ .ok();
+ }
+ })
+ .detach();
+ })
+ .ok()?;
+ }
+ 1 => {
+ workspace
+ .update(cx, move |workspace, cx| {
+ let app_state = workspace.app_state().clone();
+ let destination_path = destination_dir.clone();
+ let on_success = on_success.clone();
+
+ workspace::open_new(
+ Default::default(),
+ app_state,
+ cx,
+ move |workspace, window, cx| {
+ cx.activate(true);
+
+ let create_task =
+ workspace.project().update(cx, |project, cx| {
+ project.create_worktree(
+ destination_path.as_path(),
+ true,
+ cx,
+ )
+ });
+
+ let workspace_weak = cx.weak_entity();
+ cx.spawn_in(window, async move |_window, cx| {
+ if create_task.await.log_err().is_some() {
+ workspace_weak
+ .update_in(cx, |workspace, window, cx| {
+ (on_success)(workspace, window, cx);
+ })
+ .ok();
+ }
+ })
+ .detach();
+ },
+ )
+ .detach();
+ })
+ .ok();
+ }
+ _ => {}
+ }
+
+ Some(())
+ })
+ .detach();
+}
@@ -2849,93 +2849,15 @@ impl GitPanel {
}
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
- let path = cx.prompt_for_paths(gpui::PathPromptOptions {
- files: false,
- directories: true,
- multiple: false,
- prompt: Some("Select as Repository Destination".into()),
- });
-
let workspace = self.workspace.clone();
- cx.spawn_in(window, async move |this, cx| {
- let mut paths = path.await.ok()?.ok()??;
- let mut path = paths.pop()?;
- let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
-
- let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
-
- let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
- Ok(_) => cx.update(|window, cx| {
- window.prompt(
- PromptLevel::Info,
- &format!("Git Clone: {}", repo_name),
- None,
- &["Add repo to project", "Open repo in new project"],
- cx,
- )
- }),
- Err(e) => {
- this.update(cx, |this: &mut GitPanel, cx| {
- let toast = StatusToast::new(e.to_string(), cx, |this, _| {
- this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
- .dismiss_button(true)
- });
-
- this.workspace
- .update(cx, |workspace, cx| {
- workspace.toggle_status_toast(toast, cx);
- })
- .ok();
- })
- .ok()?;
-
- return None;
- }
- }
- .ok()?;
-
- path.push(repo_name);
- match prompt_answer.await.ok()? {
- 0 => {
- workspace
- .update(cx, |workspace, cx| {
- workspace
- .project()
- .update(cx, |project, cx| {
- project.create_worktree(path.as_path(), true, cx)
- })
- .detach();
- })
- .ok();
- }
- 1 => {
- workspace
- .update(cx, move |workspace, cx| {
- workspace::open_new(
- Default::default(),
- workspace.app_state().clone(),
- cx,
- move |workspace, _, cx| {
- cx.activate(true);
- workspace
- .project()
- .update(cx, |project, cx| {
- project.create_worktree(&path, true, cx)
- })
- .detach();
- },
- )
- .detach();
- })
- .ok();
- }
- _ => {}
- }
-
- Some(())
- })
- .detach();
+ crate::clone::clone_and_open(
+ repo.into(),
+ workspace,
+ window,
+ cx,
+ Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
+ );
}
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -10,6 +10,7 @@ use ui::{
};
mod blame_ui;
+pub mod clone;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -1141,6 +1141,104 @@ fn test_text_objects(cx: &mut App) {
)
}
+#[gpui::test]
+fn test_text_objects_with_has_parent_predicate(cx: &mut App) {
+ use std::borrow::Cow;
+
+ // Create a language with a custom text_objects query that uses #has-parent?
+ // This query only matches closure_expression when it's inside a call_expression
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_queries(LanguageQueries {
+ text_objects: Some(Cow::from(indoc! {r#"
+ ; Only match closures that are arguments to function calls
+ (closure_expression) @function.around
+ (#has-parent? @function.around arguments)
+ "#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ let (text, ranges) = marked_text_ranges(
+ indoc! {r#"
+ fn main() {
+ let standalone = |x| x + 1;
+ let result = foo(|y| y * ห2);
+ }"#
+ },
+ false,
+ );
+
+ let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+ let matches = snapshot
+ .text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
+ .map(|(range, text_object)| (&text[range], text_object))
+ .collect::<Vec<_>>();
+
+ // Should only match the closure inside foo(), not the standalone closure
+ assert_eq!(matches, &[("|y| y * 2", TextObject::AroundFunction),]);
+}
+
+#[gpui::test]
+fn test_text_objects_with_not_has_parent_predicate(cx: &mut App) {
+ use std::borrow::Cow;
+
+ // Create a language with a custom text_objects query that uses #not-has-parent?
+ // This query only matches closure_expression when it's NOT inside a call_expression
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_queries(LanguageQueries {
+ text_objects: Some(Cow::from(indoc! {r#"
+ ; Only match closures that are NOT arguments to function calls
+ (closure_expression) @function.around
+ (#not-has-parent? @function.around arguments)
+ "#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ let (text, ranges) = marked_text_ranges(
+ indoc! {r#"
+ fn main() {
+ let standalone = |x| x +ห 1;
+ let result = foo(|y| y * 2);
+ }"#
+ },
+ false,
+ );
+
+ let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
+
+ let matches = snapshot
+ .text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
+ .map(|(range, text_object)| (&text[range], text_object))
+ .collect::<Vec<_>>();
+
+ // Should only match the standalone closure, not the one inside foo()
+ assert_eq!(matches, &[("|x| x + 1", TextObject::AroundFunction),]);
+}
+
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
#[track_caller]
@@ -19,7 +19,10 @@ use std::{
use streaming_iterator::StreamingIterator;
use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
-use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
+use tree_sitter::{
+ Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatch, QueryMatches,
+ QueryPredicateArg, Tree,
+};
pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
@@ -82,6 +85,7 @@ struct SyntaxMapMatchesLayer<'a> {
next_captures: Vec<QueryCapture<'a>>,
has_next: bool,
matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
+ query: &'a Query,
grammar_index: usize,
_query_cursor: QueryCursorHandle,
}
@@ -1163,6 +1167,7 @@ impl<'a> SyntaxMapMatches<'a> {
depth: layer.depth,
grammar_index,
matches,
+ query,
next_pattern_index: 0,
next_captures: Vec::new(),
has_next: false,
@@ -1260,13 +1265,20 @@ impl SyntaxMapCapturesLayer<'_> {
impl SyntaxMapMatchesLayer<'_> {
fn advance(&mut self) {
- if let Some(mat) = self.matches.next() {
- self.next_captures.clear();
- self.next_captures.extend_from_slice(mat.captures);
- self.next_pattern_index = mat.pattern_index;
- self.has_next = true;
- } else {
- self.has_next = false;
+ loop {
+ if let Some(mat) = self.matches.next() {
+ if !satisfies_custom_predicates(self.query, mat) {
+ continue;
+ }
+ self.next_captures.clear();
+ self.next_captures.extend_from_slice(mat.captures);
+ self.next_pattern_index = mat.pattern_index;
+ self.has_next = true;
+ return;
+ } else {
+ self.has_next = false;
+ return;
+ }
}
}
@@ -1295,6 +1307,39 @@ impl<'a> Iterator for SyntaxMapCaptures<'a> {
}
}
+fn satisfies_custom_predicates(query: &Query, mat: &QueryMatch) -> bool {
+ for predicate in query.general_predicates(mat.pattern_index) {
+ let satisfied = match predicate.operator.as_ref() {
+ "has-parent?" => has_parent(&predicate.args, mat),
+ "not-has-parent?" => !has_parent(&predicate.args, mat),
+ _ => true,
+ };
+ if !satisfied {
+ return false;
+ }
+ }
+ true
+}
+
+fn has_parent(args: &[QueryPredicateArg], mat: &QueryMatch) -> bool {
+ let (
+ Some(QueryPredicateArg::Capture(capture_ix)),
+ Some(QueryPredicateArg::String(parent_kind)),
+ ) = (args.first(), args.get(1))
+ else {
+ return false;
+ };
+
+ let Some(capture) = mat.captures.iter().find(|c| c.index == *capture_ix) else {
+ return false;
+ };
+
+ capture
+ .node
+ .parent()
+ .is_some_and(|p| p.kind() == parent_kind.as_ref())
+}
+
fn join_ranges(
a: impl Iterator<Item = Range<usize>>,
b: impl Iterator<Item = Range<usize>>,
@@ -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"] }
@@ -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,9 +7,12 @@ 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;
@@ -31,6 +34,56 @@ 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) => {
+ 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 => {
+ 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>> {
@@ -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>> {
@@ -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(),
@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration (captures body for expression-bodied arrows)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(generator_function
body: (_
@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration (expression body fallback)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function
@@ -18,13 +18,48 @@
(_)* @function.inside
"}")) @function.around
-(arrow_function
+((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
+ (#not-has-parent? @function.around variable_declarator))
-(arrow_function) @function.around
+; Arrow function in variable declaration - capture the full declaration
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (statement_block
+ "{"
+ (_)* @function.inside
+ "}"))))
+]) @function.around
+
+; Arrow function in variable declaration - capture body as @function.inside
+; (for statement blocks, the more specific pattern above captures just the contents)
+([
+ (lexical_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+ (variable_declaration
+ (variable_declarator
+ value: (arrow_function
+ body: (_) @function.inside)))
+]) @function.around
+
+; Catch-all for arrow functions in other contexts (callbacks, etc.)
+((arrow_function
+ body: (_) @function.inside) @function.around
+ (#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function
@@ -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()
@@ -925,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);
@@ -1155,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
}
@@ -3864,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>,
@@ -8354,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(),
@@ -9389,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(),
@@ -11419,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(),
@@ -50,28 +50,24 @@ impl ScrollableHandle for TerminalScrollHandle {
let state = self.state.borrow();
size(
Pixels::ZERO,
- state
- .total_lines
- .checked_sub(state.viewport_lines)
- .unwrap_or(0) as f32
- * state.line_height,
+ state.total_lines.saturating_sub(state.viewport_lines) as f32 * state.line_height,
)
}
fn offset(&self) -> Point<Pixels> {
let state = self.state.borrow();
- let scroll_offset = state.total_lines - state.viewport_lines - state.display_offset;
- Point::new(
- Pixels::ZERO,
- -(scroll_offset as f32 * self.state.borrow().line_height),
- )
+ let scroll_offset = state
+ .total_lines
+ .saturating_sub(state.viewport_lines)
+ .saturating_sub(state.display_offset);
+ Point::new(Pixels::ZERO, -(scroll_offset as f32 * state.line_height))
}
fn set_offset(&self, point: Point<Pixels>) {
let state = self.state.borrow();
let offset_delta = (point.y / state.line_height).round() as i32;
- let max_offset = state.total_lines - state.viewport_lines;
+ let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
self.future_display_offset
@@ -1,12 +1,7 @@
-use gpui::{Entity, OwnedMenu, OwnedMenuItem};
+use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, actions};
use settings::Settings;
-#[cfg(not(target_os = "macos"))]
-use gpui::{Action, actions};
-
-#[cfg(not(target_os = "macos"))]
use schemars::JsonSchema;
-#[cfg(not(target_os = "macos"))]
use serde::Deserialize;
use smallvec::SmallVec;
@@ -14,18 +9,23 @@ use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use crate::title_bar_settings::TitleBarSettings;
-#[cfg(not(target_os = "macos"))]
actions!(
app_menu,
[
- /// Navigates to the menu item on the right.
+ /// Activates the menu on the right in the client-side application menu.
+ ///
+ /// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuRight,
- /// Navigates to the menu item on the left.
+ /// Activates the menu on the left in the client-side application menu.
+ ///
+ /// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuLeft
]
);
-#[cfg(not(target_os = "macos"))]
+/// Opens the named menu in the client-side application menu.
+///
+/// Does not apply to platform menu bars (e.g. on macOS).
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]
#[action(namespace = app_menu)]
pub struct OpenApplicationMenu(String);
@@ -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
+
+ 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();
+ }))
})
- .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);
+ .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(),
@@ -3407,4 +3407,390 @@ mod test {
.assert_eq(" หf = (x: unknown) => {");
cx.shared_clipboard().await.assert_eq("const ");
}
+
+ #[gpui::test]
+ async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_typescript(cx).await;
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ห1;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst foo = () => {
+ return 1;
+ };หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ arr.map(() => {
+ return ห1;
+ });
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ arr.map(ยซ() => {
+ return 1;
+ }หยป);
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ห1;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v i f");
+ cx.assert_state(
+ indoc! {"
+ const foo = () => {
+ ยซreturn 1;หยป
+ };
+ "},
+ Mode::Visual,
+ );
+
+ cx.set_state(
+ indoc! {"
+ (() => {
+ console.log(ห1);
+ })();
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ (ยซ() => {
+ console.log(1);
+ }หยป)();
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const foo = () => {
+ return ห1;
+ };
+ export { foo };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst foo = () => {
+ return 1;
+ };หยป
+ export { foo };
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ let bar = () => {
+ return ห2;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซlet bar = () => {
+ return 2;
+ };หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ var baz = () => {
+ return ห3;
+ };
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซvar baz = () => {
+ return 3;
+ };หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) => a + หb;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst add = (a, b) => a + b;หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = ห(a, b) => a + b;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst add = (a, b) => a + b;หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) => a + bห;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst add = (a, b) => a + b;หยป
+ "},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {"
+ const add = (a, b) =ห> a + b;
+ "},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {"
+ ยซconst add = (a, b) => a + b;หยป
+ "},
+ Mode::VisualLine,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new_tsx(cx).await;
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => {
+ alert("Hello world!");
+ console.log(ห"clicked");
+ }}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => {
+ alert("Hello world!");
+ console.log("clicked");
+ }หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => console.log("clickหed")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => console.log("clicked")หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ห() => console.log("clicked")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => console.log("clicked")หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => console.log("clicked"ห)}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => console.log("clicked")หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() =ห> console.log("clicked")}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => console.log("clicked")หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => {
+ console.log("cliหcked");
+ }}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => {
+ console.log("clicked");
+ }หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+
+ cx.set_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={() => fหoo()}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::Normal,
+ );
+ cx.simulate_keystrokes("v a f");
+ cx.assert_state(
+ indoc! {r#"
+ export const MyComponent = () => {
+ return (
+ <div>
+ <div onClick={ยซ() => foo()หยป}>Hello world!</div>
+ </div>
+ );
+ };
+ "#},
+ Mode::VisualLine,
+ );
+ }
}
@@ -522,12 +522,16 @@ impl Vim {
selection.start = original_point.to_display_point(map)
}
} else {
- selection.end = movement::saturating_right(
- map,
- original_point.to_display_point(map),
- );
- if original_point.column > 0 {
- selection.reversed = true
+ let original_display_point =
+ original_point.to_display_point(map);
+ if selection.end <= original_display_point {
+ selection.end = movement::saturating_right(
+ map,
+ original_display_point,
+ );
+ if original_point.column > 0 {
+ selection.reversed = true
+ }
}
}
}
@@ -15,10 +15,6 @@ tracy = ["ztracing/tracy"]
[[bin]]
name = "zed"
-path = "src/zed-main.rs"
-
-[lib]
-name = "zed"
path = "src/main.rs"
[dependencies]
@@ -1,3 +1,6 @@
+// Disable command line from opening on release mode
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
mod reliability;
mod zed;
@@ -15,11 +18,13 @@ use extension::ExtensionHostProxy;
use fs::{Fs, RealFs};
use futures::{StreamExt, channel::oneshot, future};
use git::GitHostingProviderRegistry;
+use git_ui::clone::clone_and_open;
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use onboarding::{FIRST_OPEN, show_onboarding_view};
+use project_panel::ProjectPanel;
use prompt_store::PromptBuilder;
use remote::RemoteConnectionOptions;
use reqwest_client::ReqwestClient;
@@ -33,10 +38,12 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use session::{AppSession, Session};
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
use std::{
+ cell::RefCell,
env,
io::{self, IsTerminal},
path::{Path, PathBuf},
process,
+ rc::Rc,
sync::{Arc, OnceLock},
time::Instant,
};
@@ -163,9 +170,9 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
.detach();
}
}
-pub static STARTUP_TIME: OnceLock<Instant> = OnceLock::new();
+static STARTUP_TIME: OnceLock<Instant> = OnceLock::new();
-pub fn main() {
+fn main() {
STARTUP_TIME.get_or_init(|| Instant::now());
#[cfg(unix)]
@@ -893,6 +900,41 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
})
.detach_and_log_err(cx);
}
+ OpenRequestKind::GitClone { repo_url } => {
+ workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
+ if window.is_window_active() {
+ clone_and_open(
+ repo_url,
+ cx.weak_entity(),
+ window,
+ cx,
+ Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
+ workspace.focus_panel::<ProjectPanel>(window, cx);
+ }),
+ );
+ return;
+ }
+
+ let subscription = Rc::new(RefCell::new(None));
+ subscription.replace(Some(cx.observe_in(&cx.entity(), window, {
+ let subscription = subscription.clone();
+ let repo_url = repo_url;
+ move |_, workspace_entity, window, cx| {
+ if window.is_window_active() && subscription.take().is_some() {
+ clone_and_open(
+ repo_url.clone(),
+ workspace_entity.downgrade(),
+ window,
+ cx,
+ Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
+ workspace.focus_panel::<ProjectPanel>(window, cx);
+ }),
+ );
+ }
+ }
+ })));
+ });
+ }
OpenRequestKind::GitCommit { sha } => {
cx.spawn(async move |cx| {
let paths_with_position =
@@ -1301,7 +1343,7 @@ fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
})
}
-pub fn stdout_is_a_pty() -> bool {
+fn stdout_is_a_pty() -> bool {
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
}
@@ -1547,14 +1589,14 @@ fn dump_all_gpui_actions() {
struct ActionDef {
name: &'static str,
human_name: String,
- aliases: &'static [&'static str],
+ deprecated_aliases: &'static [&'static str],
documentation: Option<&'static str>,
}
let mut actions = gpui::generate_list_of_all_registered_actions()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
- aliases: action.deprecated_aliases,
+ deprecated_aliases: action.deprecated_aliases,
documentation: action.documentation,
})
.collect::<Vec<ActionDef>>();
@@ -1,8 +0,0 @@
-// Disable command line from opening on release mode
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-
-pub fn main() {
- // separated out so that the file containing the main function can be imported by other crates,
- // while having all gpui resources that are registered in main (primarily actions) initialized
- zed::main();
-}
@@ -4780,7 +4780,6 @@ mod tests {
"activity_indicator",
"agent",
"agents",
- #[cfg(not(target_os = "macos"))]
"app_menu",
"assistant",
"assistant2",
@@ -25,6 +25,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
+use ui::SharedString;
use util::ResultExt;
use util::paths::PathWithPosition;
use workspace::PathList;
@@ -58,6 +59,9 @@ pub enum OpenRequestKind {
/// `None` opens settings without navigating to a specific path.
setting_path: Option<String>,
},
+ GitClone {
+ repo_url: SharedString,
+ },
GitCommit {
sha: String,
},
@@ -113,6 +117,8 @@ impl OpenRequest {
this.kind = Some(OpenRequestKind::Setting {
setting_path: Some(setting_path.to_string()),
});
+ } else if let Some(clone_path) = url.strip_prefix("zed://git/clone") {
+ this.parse_git_clone_url(clone_path)?
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
this.parse_git_commit_url(commit_path)?
} else if url.starts_with("ssh://") {
@@ -143,6 +149,26 @@ impl OpenRequest {
}
}
+ fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> {
+ // Format: /?repo=<url> or ?repo=<url>
+ let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);
+
+ let query = clone_path
+ .strip_prefix('?')
+ .context("invalid git clone url: missing query string")?;
+
+ let repo_url = url::form_urlencoded::parse(query.as_bytes())
+ .find_map(|(key, value)| (key == "repo").then_some(value))
+ .filter(|s| !s.is_empty())
+ .context("invalid git clone url: missing repo query parameter")?
+ .to_string()
+ .into();
+
+ self.kind = Some(OpenRequestKind::GitClone { repo_url });
+
+ Ok(())
+ }
+
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
// Format: <sha>?repo=<path>
let (sha, query) = commit_path
@@ -1087,4 +1113,80 @@ mod tests {
assert!(!errored_reuse);
}
+
+ #[gpui::test]
+ fn test_parse_git_clone_url(cx: &mut TestAppContext) {
+ let _app_state = init_test(cx);
+
+ let request = cx.update(|cx| {
+ OpenRequest::parse(
+ RawOpenRequest {
+ urls: vec![
+ "zed://git/clone/?repo=https://github.com/zed-industries/zed.git".into(),
+ ],
+ ..Default::default()
+ },
+ cx,
+ )
+ .unwrap()
+ });
+
+ match request.kind {
+ Some(OpenRequestKind::GitClone { repo_url }) => {
+ assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
+ }
+ _ => panic!("Expected GitClone kind"),
+ }
+ }
+
+ #[gpui::test]
+ fn test_parse_git_clone_url_without_slash(cx: &mut TestAppContext) {
+ let _app_state = init_test(cx);
+
+ let request = cx.update(|cx| {
+ OpenRequest::parse(
+ RawOpenRequest {
+ urls: vec![
+ "zed://git/clone?repo=https://github.com/zed-industries/zed.git".into(),
+ ],
+ ..Default::default()
+ },
+ cx,
+ )
+ .unwrap()
+ });
+
+ match request.kind {
+ Some(OpenRequestKind::GitClone { repo_url }) => {
+ assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
+ }
+ _ => panic!("Expected GitClone kind"),
+ }
+ }
+
+ #[gpui::test]
+ fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) {
+ let _app_state = init_test(cx);
+
+ let request = cx.update(|cx| {
+ OpenRequest::parse(
+ RawOpenRequest {
+ urls: vec![
+ "zed://git/clone/?repo=https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git"
+ .into(),
+ ],
+ ..Default::default()
+ },
+ cx,
+ )
+ .unwrap()
+ });
+
+ match request.kind {
+ Some(OpenRequestKind::GitClone { repo_url }) => {
+ assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
+ }
+ _ => panic!("Expected GitClone kind"),
+ }
+ }
}
@@ -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)
@@ -46,7 +46,7 @@ Having a series of rules files specifically tailored to prompt engineering can a
Here are a couple of helpful resources for writing better rules:
-- [Anthropic: Prompt Engineering](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview)
+- [Anthropic: Prompt Engineering](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview)
- [OpenAI: Prompt Engineering](https://platform.openai.com/docs/guides/prompt-engineering)
### Editing the Default Rules {#default-rules}
@@ -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.
@@ -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:
@@ -8,6 +8,6 @@
},
"devDependencies": {
"danger": "13.0.4",
- "danger-plugin-pr-hygiene": "0.7.0"
+ "danger-plugin-pr-hygiene": "0.7.1"
}
}
@@ -12,8 +12,8 @@ importers:
specifier: 13.0.4
version: 13.0.4
danger-plugin-pr-hygiene:
- specifier: 0.7.0
- version: 0.7.0
+ 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.7.0:
- resolution: {integrity: sha512-YDWhEodP0fg/t9YO3SxufWS9j1Rcxbig+1flTlUlojBDFiKQyVmaj8PIvnJxJItjHWTlNKI9wMSRq5vUql6zyA==}
+ 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.7.0: {}
+ danger-plugin-pr-hygiene@0.7.1: {}
danger@13.0.4:
dependencies:
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+echo "Generating action metadata..."
+cargo run -p zed -- --dump-all-actions > crates/docs_preprocessor/actions.json
+
+echo "Generated crates/docs_preprocessor/actions.json with $(grep -c '"name":' crates/docs_preprocessor/actions.json) actions"
@@ -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)),
}
}
@@ -513,6 +448,7 @@ fn check_docs() -> NamedJob {
lychee_link_check("./docs/src/**/*"), // check markdown links
)
.map(steps::install_linux_dependencies)
+ .add_step(steps::script("./script/generate-action-metadata"))
.add_step(install_mdbook())
.add_step(build_docs())
.add_step(
@@ -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"))
}