diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml index d2e62d5b22ee49c7dcb9b42085a648098fbdb6bb..1ff271f73ff6b800ec3a94615f31c35a7729bb47 100644 --- a/.github/actions/build_docs/action.yml +++ b/.github/actions/build_docs/action.yml @@ -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: diff --git a/.github/workflows/extension_tests.yml b/.github/workflows/extension_tests.yml index 7a7fff9b97d694c1b02dd426f5d59301fe2be81e..9f0917e388c74cffed8f342f7504bc111e6f5147 100644 --- a/.github/workflows/extension_tests.yml +++ b/.github/workflows/extension_tests.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 317d5a8df37a62887ce4ddcdd67c8d77b48d56d6..ffc2554a55e00a5bdb7bd1ee0bfeebd5667755d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index b23e4b7518a672c0d586ea5ba437db5cf8f94bb6..d76244175accc3e816cbd7d5dc322d2529a0a236 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -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 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index fac3221d63a080fa53b7ba1c5b7249e6a405c73c..47a84574e7c33fb8a40a90c67cd4f7dadb356978 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index 54faaf1374299ee8f97925a95a93b375c349d707..c71417c32bff76af9d4c9c67661556e1625c9d15 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ DerivedData/ Packages xcuserdata/ +crates/docs_preprocessor/actions.json # Don't commit any secrets to the repo. .env diff --git a/Cargo.lock b/Cargo.lock index 0d83b2b9b912ab112d9b38fd1ef1d5ff21f9049c..3f7077e721e934cd6cb05af0cdaefef75602b429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/README.md b/README.md index d3a5fd20526e5eae6826241dce2bb94e8533ecb3..866762c8c9139666993c2e29d9682966106c516b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index a670ba601159ec323ad2c88695c30bf4aeae4118..598d0428174eb2fc124739a18ddeff1098521cb7 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -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, - pub icon: Option, + pub icon: Option, } impl From for AgentModelInfo { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 43ed3b90f3556eb24e45440a7fe0038e7a1b9535..4baa7f4ea4004d2137b5cddb255346fa91523091 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -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::>(); @@ -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::>(); @@ -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 + )), }] )]) ); diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs index 1f50ce74321d393ba6c7f5083bd889bc3dc2c0e1..22af75a6e96edc4f597819e04e2e84b80ba0417a 100644 --- a/crates/agent_ui/src/acp/mode_selector.rs +++ b/crates/agent_ui/src/acp/mode_selector.rs @@ -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, )), diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index f3c07250de3cefc798b97d9ffad444489d153219..903d5fe425d99389aae0e2a8028d9a31b986fbb3 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -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| { diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index d6709081863c9545fba4c6e2304f195e77b013df..a15c01445dd8e9845f6744e795ed90a1ede6c7fc 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -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) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 8364fd8c0f4d8fd55df8f2e74e990e603029db78..32b2de2c0d850676bf7a6a80ee88950d62aa24e0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -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); })) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 24f019c605d1b167e62a6e68dfc1f3ed07c73f1c..562976453d963db65f9033536e528000de2b510f 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -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) { - 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, ) -> 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( diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index ac57ed575d9d1b6de2c53d3e0e4a91b4bd16ab1a..45cefbf2b9f8d4b1639a9849f2ee2e4468e530b1 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -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) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 294cd8b4888950f6ea92d6bea1eba78c3d6d6de2..a050f75120cd73949251c09c8424314e3616c705 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -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) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 02cb7e59948b10274302bd8cd6f74f1accbd30a3..401b506b302d9c2a86a36ddce0fc72df075f4c18 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -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); } _ => {} diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 77c8c95255908dc54639ad7ac6c55f1e8b8151f0..704e340ace35f33f757ab7708f96ffc940a8eb91 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -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> #[derive(Clone)] struct ModelInfo { model: Arc, - 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::>(); @@ -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::>(); @@ -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, } }) diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ac08070fcefa92854b51bc8a66d4d388d08e087d..327d2c67e2d5e87e67935ecdfa7fb6cd41acbcb5 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -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() } }), diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 16d12cf261d3bbb8eb0b879394fedc1cc96e046c..514f45528427af89eeccf85512abf850a7a1be05 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -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) diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs index 061b4f58288798696b068a091fb392c033906627..beb0c13d761aa9e7e41c2ac4e35a8cfcc7e8d869 100644 --- a/crates/agent_ui/src/ui/model_selector_components.rs +++ b/crates/agent_ui/src/ui/model_selector_components.rs @@ -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, + icon: Option, 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()), diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index fadc4222ae44f3dbad862fd9479b89321dbd3016..47197ec2331b97dd4d7561d9f14c91c7f91c9fa0 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -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() diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 3c8ffc1663e0660829698b5449a006de5b3c6009..c2756927136449d649996ec3b4b87471114aca38 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, client: Arc, - configured_providers: Vec<(IconName, SharedString)>, + has_configured_providers: bool, continue_with_zed_ai: Arc, } @@ -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()) diff --git a/crates/docs_preprocessor/Cargo.toml b/crates/docs_preprocessor/Cargo.toml index e71f9ae3f3f6fcff790db27fb1e377f0d1c20e40..07da23899956822f7577118ae85b6338b4cefae7 100644 --- a/crates/docs_preprocessor/Cargo.toml +++ b/crates/docs_preprocessor/Cargo.toml @@ -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" \ No newline at end of file diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index b614a8251139413f4b316937db1d4e3c0d551df6..d90dcc10db9fbd8d27a968094ea8d733a79b7e80 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") }); -static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); +static ALL_ACTIONS: LazyLock> = LazyLock::new(load_all_actions); const FRONT_MATTER_COMMENT: &str = ""; 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::>(); 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{}", name); }; format!("{}", &action.human_name) }) @@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet 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 { let keymap = match os { "macos" => &KEYMAP_MACOS, @@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet
, _>>()
-                            .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,
+    #[serde(rename = "documentation")]
+    docs: Option,
 }
 
-fn dump_all_gpui_actions() -> Vec {
-    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::>();
-
-    actions.sort_by_key(|a| a.name);
-
-    actions
+fn load_all_actions() -> Vec {
+    let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
+    match std::fs::read_to_string(asset_path) {
+        Ok(content) => {
+            let mut actions: Vec =
+                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::>();
-    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("
\n"); @@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String { output.push_str("
\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("
\n"); } output.push_str("Keymap Name: "); - output.push_str(action.name); + output.push_str(&action.name); output.push_str("
\n"); if !action.deprecated_aliases.is_empty() { output.push_str("Deprecated Alias(es): "); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 7c4c0e48d36dbb9f74a1c835c63fa2b91c5681d9..3e7c47c2ac5efeedde51f180bcfcb424aec31c86 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -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"); diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 6a24e3ba3f496bd0f0b89d61e9125b29ecae0204..b445878389015d4b3b8c3e25a0d103586462fd86 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -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; + #[derive(Default)] pub struct ExtensionHostProxy { theme_proxy: RwLock>>, @@ -29,6 +32,7 @@ pub struct ExtensionHostProxy { slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, debug_adapter_provider_proxy: RwLock>>, + language_model_provider_proxy: RwLock>>, } 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, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ); + + fn unregister_language_model_provider(&self, provider_id: Arc, cx: &mut App); +} + +impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy { + fn register_language_model_provider( + &self, + provider_id: Arc, + 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, cx: &mut App) { + let Some(proxy) = self.language_model_provider_proxy.read().clone() else { + return; + }; + + proxy.unregister_language_model_provider(provider_id, cx) + } +} diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 4ecdd378ca86dbee263e439e13fa4776dab9e316..39b629db30d0d1cee3374dafc317bdeb0f368146 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -93,6 +93,8 @@ pub struct ExtensionManifest { pub debug_adapters: BTreeMap, DebugAdapterManifestEntry>, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub debug_locators: BTreeMap, DebugLocatorManifestEntry>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub language_model_providers: BTreeMap, 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, +} + impl ExtensionManifest { pub async fn load(fs: Arc, extension_dir: &Path) -> Result { 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(), } } diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs index a28f617dc36e5cba3ad36d7ab6477e7a665dd5c4..605b98c67071155d8444639ef7043b9c8901161d 100644 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ b/crates/extension_host/benches/extension_compilation_benchmark.rs @@ -148,6 +148,7 @@ fn manifest() -> ExtensionManifest { )], debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), } } diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 9f27b5e480bc3c22faefe67cd49a06af21614096..6278deef0a7d41e40d4444ddbe992f007cd5e53e 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -113,6 +113,7 @@ mod tests { capabilities: vec![], debug_adapters: Default::default(), debug_locators: Default::default(), + language_model_providers: BTreeMap::default(), } } diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 54b090347ffad3ffed444827f5cb60c120d25ad7..c17484f26a06b3392cdbcd8f3c1578eb43c7b213 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -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, }, diff --git a/crates/git_ui/src/clone.rs b/crates/git_ui/src/clone.rs new file mode 100644 index 0000000000000000000000000000000000000000..a6767d33304d3f20b7a5e78340f62c89ebe3ae58 --- /dev/null +++ b/crates/git_ui/src/clone.rs @@ -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, + window: &mut Window, + cx: &mut App, + on_success: Arc< + dyn Fn(&mut Workspace, &mut Window, &mut Context) + 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(); +} diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 0f967e68d1fab829fb37b626c23ecfebe69fb5dd..532f9a099a823796706be48ed14cc7da820c5d8b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2849,93 +2849,15 @@ impl GitPanel { } pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context) { - 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) { diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 5f50e4ef8029d8f57cd159bc7da68b668b628f48..053c41bf10c5d97f9f5326fd17d6b5bf91297a03 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -10,6 +10,7 @@ use ui::{ }; mod blame_ui; +pub mod clone; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 54e2ef4065460547f4a3f86db7d3a3986dff65eb..2c2d93c8239f0f3fcb1de0956de2d3400f13e96b 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -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::>(); + + // 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::>(); + + // 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] diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 77e90c4ca89d0b6e5b8cb0a604175ec9a97e719e..db4ab4f459c35a98752bef1eb5be558084b5c906 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -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>, 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>, b: impl Iterator>, diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 09d44b5b408324936af00a2a5e4f1deb4f351434..56a970404419ec6042c463d26c2844eb0904f829 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -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>; fn default_fast_model(&self, cx: &App) -> Option>; @@ -820,7 +835,7 @@ pub trait LanguageModelProvider: 'static { fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[derive(Default, Clone)] +#[derive(Default, Clone, PartialEq, Eq)] pub enum ConfigurationViewTargetAgent { #[default] ZedAgent, diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 27b8309810962981d3c0ec78e6e67dfdfba122bf..cf7718f7b102010cc0c8a981a0425583436176b7 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -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 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, providers: BTreeMap>, inline_alternatives: Vec>, + /// Set of installed extension IDs that provide language models. + /// Used to determine which built-in providers should be hidden. + installed_llm_extension_ids: HashSet>, + /// Function to check if a built-in provider should be hidden by an extension. + builtin_provider_hiding_fn: Option, } #[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 for LanguageModelRegistry {} @@ -183,6 +194,60 @@ impl LanguageModelRegistry { providers } + /// Returns providers, filtering out hidden built-in providers. + pub fn visible_providers(&self) -> Vec> { + 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, cx: &mut Context) { + 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) { + 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>, + cx: &mut Context, + ) { + 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, @@ -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); + } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 5531e698ab7fccae736e800f38b16e35bcd35ac4..1bec5d94d2bb35f91305c6c77a9e85ed8579e1af 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -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"] } diff --git a/crates/language_models/src/extension.rs b/crates/language_models/src/extension.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0b46ab5e1d667fb61449a654769ecf7c221e720 --- /dev/null +++ b/crates/language_models/src/extension.rs @@ -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> = + 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, +} + +impl LanguageModelProviderRegistryProxy { + pub fn new(registry: Entity) -> Self { + Self { registry } + } +} + +impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy { + fn register_language_model_provider( + &self, + _provider_id: Arc, + register_fn: LanguageModelProviderRegistration, + cx: &mut App, + ) { + register_fn(cx); + } + + fn unregister_language_model_provider(&self, provider_id: Arc, 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)); +} diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 1038f5e233e0a5970b0e8bd969a65f6f0e2a7550..37d4ca5ddd4e5c1e7a0202c88c012d18b018cd4f 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -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, client: Arc, 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() diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index d8c972399c33922386bfba4236e1369d03d338dc..598834f85c496cd54ddd956089715cac64420202 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -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> { diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 286f9ec1a4bf67c22868cf83e00e7b46e0737ba8..62237fbf376a0739fd2518bda44f51149b3457df 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -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> { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index def1cef84d3166d08dcc7638ca5a29cabbd149c5..65a42740eb9a8aff830d7544ed5aa972c6697d88 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -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> { diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 70198b337e467e1618192e781d3e3be305fea9c5..68eaab1dbed33a8d983de6a919b75dc809410a70 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -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> { diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index b00a5d82f5665a5c87c662d1af84fbeb9ac07ebb..b3264b869195aa34d7083cd31992d8c220d20349 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -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> { diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 989b99061b6d0f4c6680f08616c55946138ae0fe..7d567d60f405c7880cb6494f6d2ff604d7f53ac2 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -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> { diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 94f99f10afc8928fb7fbc8526ab46e7dca37a5ce..237b64ac7d0ed728b057f6b553ad2a2a1ebae1db 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -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> { diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 64f3999e3aa96b2611e265a6eaf5df8063332c2a..0b8af405ade8fc00c0d1e2e57ba115560d94a71d 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -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> { diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index c5a8bf41711563110cbcb5d81698b7029b04a713..f5d8820e710ea6c9f89de6da5a7aae2f204c6470 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -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> { @@ -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, + 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 = 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); + } +} diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index afaffba3e53eb2496f9fae795d69b9e9c9f57249..905d2b37862eebf57c7fb56a540b388338cfd065 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -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> { diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index e6e7a9984da3d48b9e3c0f9571b8e916359fba03..f95f567739d76670d3cfa7b835bbfaf34ddef92f 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -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> { diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index ad2e90d9dd5f4ece7e2582a867da50f6962c981c..48d68ddebff7e0c9bbe39dbca696dd2ffcf62605 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -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> { diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 4dfe848df80123dc4c37d27b81f76db359e076f9..e2e692eafff94c56d481dfc2bd96dbfa7adda262 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -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> { diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 19c50d71cf4e483b68d48c8b982a975f3091ff46..f0aa0e71a83ae1a201d76a33f63ca0aadc6936a9 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -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> { diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index 335381c6f79d950498a0f0c1d330cb21c681f32e..7775586bf19539e13adc6b9df6d92914be6b7f21 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -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::>() + }) + .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()), ) }), )); diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index e34bbb46d35d5a524c08369fcc991dfe81865127..2b2575912ae4543d2bf3cbd0c6b667ace7c82e91 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -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(), diff --git a/crates/languages/src/javascript/textobjects.scm b/crates/languages/src/javascript/textobjects.scm index 1a273ddb5000ba920868272bb4ac31d270095442..eace658e6b9847bcc651deedad2bc27cbfbf6975 100644 --- a/crates/languages/src/javascript/textobjects.scm +++ b/crates/languages/src/javascript/textobjects.scm @@ -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: (_ diff --git a/crates/languages/src/tsx/textobjects.scm b/crates/languages/src/tsx/textobjects.scm index 836fed35ba1c1093b84e48a8da19d89177a69944..628a921f3ac9ea04ff59654d72caf73cebbc9071 100644 --- a/crates/languages/src/tsx/textobjects.scm +++ b/crates/languages/src/tsx/textobjects.scm @@ -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 diff --git a/crates/languages/src/typescript/textobjects.scm b/crates/languages/src/typescript/textobjects.scm index 836fed35ba1c1093b84e48a8da19d89177a69944..96289f058cd7b605a8f5b4c8966e3c372022d065 100644 --- a/crates/languages/src/typescript/textobjects.scm +++ b/crates/languages/src/typescript/textobjects.scm @@ -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 diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index faa094153d4a26fb1a2b96360f2691989e81aad9..36938f62a3048b87dd890ca6e7ca8fc2499689e4 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -89,6 +89,7 @@ pub struct LanguageServer { outbound_tx: channel::Sender, notification_tx: channel::Sender, name: LanguageServerName, + version: Option, process_name: Arc, binary: LanguageServerBinary, capabilities: RwLock, @@ -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 { + self.version.clone() + } + pub fn process_name(&self) -> &str { &self.process_name } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 5093b6977a1bffe82339ede00d2e6e4b4b14b4c1..7e8624daad628fd653326647537eb51dad208a02 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3864,6 +3864,7 @@ pub enum LspStoreEvent { #[derive(Clone, Debug, Serialize)] pub struct LanguageServerStatus { pub name: LanguageServerName, + pub server_version: Option, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, pub progress_tokens: HashSet, @@ -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(), diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs index 871bb602306cccc92b8cffe62c4912c42b7a87e2..82ca0b4097dad1be899879b0241aed50d8e60bfa 100644 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ b/crates/terminal_view/src/terminal_scrollbar.rs @@ -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 { 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) { 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 diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 817b73c45ecd2df4a76e9a67f425b2b459c0c026..579e4dadbd590981a4aee15019bbe73e2bb28d5c 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -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); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 756a2a9364193d6f1cdace8ed8c92cecf401a864..7e5e9032c9d4b0521f972b47d90d24cd502faf7b 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -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() } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 1c8e36ec18d6184b38eb6772e8f5a13be181ae00..9d2c7ae3b515744125879f4a2c0e0d3e9a4fb841 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -126,17 +126,6 @@ enum IconSource { ExternalSvg(SharedString), } -impl IconSource { - fn from_path(path: impl Into) -> 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) -> 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(), diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 02150332405c6d5ea4d5dd78f477348be968fddf..e9a2f4fc63d31f78a9a7abce8aac785b56eb1fd4 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -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 ( +
+
{ + alert("Hello world!"); + console.log(ˇ"clicked"); + }}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
{ + alert("Hello world!"); + console.log("clicked"); + }ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clickˇed")}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked"ˇ)}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
console.log("clicked")ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
{ + console.log("cliˇcked"); + }}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
{ + console.log("clicked"); + }ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + + cx.set_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
fˇoo()}>Hello world!
+
+ ); + }; + "#}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a f"); + cx.assert_state( + indoc! {r#" + export const MyComponent = () => { + return ( +
+
foo()ˇ»}>Hello world!
+
+ ); + }; + "#}, + Mode::VisualLine, + ); + } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 3c6f237435e3924a907e059ed1a878641c287e7e..5667190bb7239ee3e534a5556d96452a7c68b1ef 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -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 + } } } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fd160759f4440e2736d57cea62abb6bdb138ae72..80eca20e00309bb8d22552287a1c39cb9891307d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,10 +15,6 @@ tracy = ["ztracing/tracy"] [[bin]] name = "zed" -path = "src/zed-main.rs" - -[lib] -name = "zed" path = "src/main.rs" [dependencies] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7008e491c5e2ade35fa96cafbd9d8969c008fa96..03e02bb0107d736c07eb3fc9626856943f8d80a6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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 = OnceLock::new(); +static STARTUP_TIME: OnceLock = 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, 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::(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::(window, cx); + }), + ); + } + } + }))); + }); + } OpenRequestKind::GitCommit { sha } => { cx.spawn(async move |cx| { let paths_with_position = @@ -1301,7 +1343,7 @@ fn init_paths() -> HashMap> { }) } -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::>(); diff --git a/crates/zed/src/zed-main.rs b/crates/zed/src/zed-main.rs deleted file mode 100644 index 6c49c197dda01e97828c3662aa09ecf57804dfbc..0000000000000000000000000000000000000000 --- a/crates/zed/src/zed-main.rs +++ /dev/null @@ -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(); -} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d088df00839814e32a9c246a3486ac5ad5ca4b9e..3441cb88d96b06dfdbb65a58553d2c58f435d157 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4780,7 +4780,6 @@ mod tests { "activity_indicator", "agent", "agents", - #[cfg(not(target_os = "macos"))] "app_menu", "assistant", "assistant2", diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index d61de0a291f3d3e7869225c0e07424cc3523f69b..842f98520133c70f711d84d3f490bec1ec59e16f 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -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, }, + 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= or ?repo= + 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: ?repo= 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"), + } + } } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1f9c5750ea76b35a2f7f5464b7b6684401108d2b..a82ddac990c4379df03db2b4bdcd8272eb8715e9 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -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) diff --git a/docs/src/ai/rules.md b/docs/src/ai/rules.md index 4169920425e66eb41a895deb60da3a198d74df08..972bbc94e82937502739cf585cc8f60dbcda8808 100644 --- a/docs/src/ai/rules.md +++ b/docs/src/ai/rules.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} diff --git a/docs/src/development.md b/docs/src/development.md index 31bb245ac42f80c830a0faba405323d1097e3f51..8f341dbb1506d4a6fa6c3ffa21960191ec5ecfcf 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -6,10 +6,6 @@ See the platform-specific instructions for building Zed from source: - [Linux](./development/linux.md) - [Windows](./development/windows.md) -If you'd like to develop collaboration features, additionally see: - -- [Local Collaboration](./development/local-collaboration.md) - ## Keychain access Zed stores secrets in the system keychain. diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index df3b840fa17a547efd4324f3bdaa119b8ade8738..3269d4b4dd51b224ab2b0cf7cfe15333232d0915 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -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. diff --git a/docs/src/development/local-collaboration.md b/docs/src/development/local-collaboration.md deleted file mode 100644 index 393c6f0bbf797cf9aa86d297633734444bdfb328..0000000000000000000000000000000000000000 --- a/docs/src/development/local-collaboration.md +++ /dev/null @@ -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) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index 9c99e5f8da62594c774e109e15f914788f51793d..9e2908dd6e393acd8d3903d86743dcbc4e9ae9eb 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -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/). diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 17382e0bee5b97c2ffc2d74794cf3881a3cb98a1..509f30a05b45175f7e66026aec5b5d433b928e4d 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -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: diff --git a/script/danger/package.json b/script/danger/package.json index 74862c142468c1297a1d4aad8dcc468b6ddf5798..be44da6233a1c5ee87f8445e13953031497acfa5 100644 --- a/script/danger/package.json +++ b/script/danger/package.json @@ -8,6 +8,6 @@ }, "devDependencies": { "danger": "13.0.4", - "danger-plugin-pr-hygiene": "0.7.0" + "danger-plugin-pr-hygiene": "0.7.1" } } diff --git a/script/danger/pnpm-lock.yaml b/script/danger/pnpm-lock.yaml index 942d027cc80bf5d81ffa8b5bf739963430e939a3..eea293cfed78fcf43ed926484b2f13b5b9c74843 100644 --- a/script/danger/pnpm-lock.yaml +++ b/script/danger/pnpm-lock.yaml @@ -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: diff --git a/script/generate-action-metadata b/script/generate-action-metadata new file mode 100755 index 0000000000000000000000000000000000000000..146b1f0d78ef92c47322a70dccf0e9e1f3f530d3 --- /dev/null +++ b/script/generate-action-metadata @@ -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" diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index d0caab82b057f21735b7f828c8917a358dd548b2..aceb575b647e7ea0b2d8a74da9fbc153767d149d 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -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 { 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 { - 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 { 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( diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs index eaa51dc35205f51e7fe3a56668ed0679e92999f0..a0b071cd6c31654b42adddbba47dd24c60da7df2 100644 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ b/tooling/xtask/src/tasks/workflows/steps.rs @@ -54,25 +54,12 @@ pub fn setup_sentry() -> Step { .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 { - named::bash("./script/prettier").id(PRETTIER_STEP_ID) + named::bash("./script/prettier") } pub fn cargo_fmt() -> Step { - named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID) -} - -pub fn record_style_failure() -> Step { - 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 { @@ -118,25 +105,13 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step { } } -pub const CLIPPY_STEP_ID: &str = "clippy"; -pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure"; - pub fn clippy(platform: Platform) -> Step { 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 { - 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 { named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust")) }