Compare commits
32 Commits
remote-pro
...
restore-hu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54dc913a79 | ||
|
|
ae44c3c881 | ||
|
|
4e0471cf66 | ||
|
|
62d36b22fd | ||
|
|
69f6eeaa3a | ||
|
|
1dc5de4592 | ||
|
|
b9aef75f2d | ||
|
|
95ae388c0c | ||
|
|
1ac170e663 | ||
|
|
3104482c6c | ||
|
|
7ee56e1a18 | ||
|
|
f2495a6f98 | ||
|
|
6d776c3157 | ||
|
|
596826f741 | ||
|
|
e44529ed7b | ||
|
|
e052127e1c | ||
|
|
0531035b86 | ||
|
|
05ce34eea4 | ||
|
|
63c4406137 | ||
|
|
3f67c5220d | ||
|
|
435d4c5f24 | ||
|
|
e0ff995e2d | ||
|
|
6976208e21 | ||
|
|
6055b45ee1 | ||
|
|
88f90c12ed | ||
|
|
0d74f982a5 | ||
|
|
ca90b8555d | ||
|
|
8516d81e13 | ||
|
|
af589ff25f | ||
|
|
d2bbfbb3bf | ||
|
|
413f4ea49c | ||
|
|
1b6d588413 |
12
.github/actions/build_docs/action.yml
vendored
12
.github/actions/build_docs/action.yml
vendored
@@ -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:
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 31 DEC *"
|
||||
- cron: "0 2 * * 5"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug-only:
|
||||
description: "Run in dry-run mode (no changes made)"
|
||||
type: boolean
|
||||
default: false
|
||||
operations-per-run:
|
||||
description: "Max number of issues to process (default: 1000)"
|
||||
type: number
|
||||
default: 1000
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
|
||||
Hi there!
|
||||
Zed development moves fast and a significant number of bugs become outdated.
|
||||
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
|
||||
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
operations-per-run: 1000
|
||||
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
debug-only: ${{ inputs.debug-only }}
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,8 +61,7 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: extension_tests::run_clippy
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -26,8 +26,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -72,15 +71,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -94,8 +87,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -114,8 +105,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,8 +20,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -45,8 +44,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
50
.github/workflows/run_tests.yml
vendored
50
.github/workflows/run_tests.yml
vendored
@@ -74,19 +74,12 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
- name: steps::prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_style_failure
|
||||
name: steps::record_style_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -97,8 +90,6 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
outputs:
|
||||
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -119,8 +110,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -167,15 +157,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -189,8 +173,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_mac:
|
||||
needs:
|
||||
@@ -211,8 +193,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,6 +36,7 @@
|
||||
DerivedData/
|
||||
Packages
|
||||
xcuserdata/
|
||||
crates/docs_preprocessor/actions.json
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
|
||||
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -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",
|
||||
@@ -12477,6 +12476,7 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"encoding_rs",
|
||||
"extension",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
@@ -12570,6 +12570,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -20264,6 +20265,16 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
|
||||
@@ -198,6 +198,7 @@ members = [
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
@@ -20,7 +20,6 @@ Other platforms are not yet available:
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -210,12 +210,21 @@ pub trait AgentModelSelector: 'static {
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon for a model in the model selector.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AgentModelIcon {
|
||||
/// A built-in icon from Zed's icon set.
|
||||
Named(IconName),
|
||||
/// Path to a custom SVG icon file.
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<IconName>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
||||
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
@@ -93,7 +93,7 @@ impl LanguageModels {
|
||||
fn refresh_list(&mut self, cx: &App) {
|
||||
let providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -153,7 +153,10 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(provider.icon()),
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1630,7 +1633,9 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -186,6 +186,17 @@ impl Render for ModeSelector {
|
||||
move |_window, cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
@@ -200,17 +211,6 @@ impl Render for ModeSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
@@ -350,7 +350,11 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
|
||||
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
@@ -70,7 +70,7 @@ impl Render for AcpModelSelectorPopover {
|
||||
.map(|model| model.name.clone())
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon);
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -125,7 +125,14 @@ impl Render for AcpModelSelectorPopover {
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(
|
||||
match icon {
|
||||
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
|
||||
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -2718,7 +2718,7 @@ impl AcpThreadView {
|
||||
..default_markdown_style(false, true, window, cx)
|
||||
},
|
||||
))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.tooltip(Tooltip::text("Go to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
|
||||
@@ -22,7 +22,8 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -117,7 +118,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -261,9 +262,12 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -416,7 +420,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -103,7 +104,14 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -115,7 +123,7 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::Small),
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
|
||||
@@ -2428,7 +2428,7 @@ impl AgentPanel {
|
||||
let history_is_empty = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
|
||||
@@ -348,7 +348,8 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|_, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -55,7 +55,7 @@ pub fn language_model_selector(
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.providers();
|
||||
let providers = lm_registry.visible_providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
icon: IconOrSvg,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -474,7 +474,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -566,7 +566,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.icon(model_info.icon)
|
||||
.map(|this| match &model_info.icon {
|
||||
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
|
||||
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
@@ -702,7 +705,7 @@ mod tests {
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
is_favorite,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -191,6 +191,9 @@ impl Render for ProfileSelector {
|
||||
let container = || h_flex().gap_1().justify_between();
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.child(
|
||||
container()
|
||||
.pb_1()
|
||||
@@ -203,9 +206,6 @@ impl Render for ProfileSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -33,7 +33,8 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -2231,10 +2232,10 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
@@ -2244,6 +2245,13 @@ impl TextThreadEditor {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let provider_icon_element = match provider_icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -2291,7 +2299,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(provider_icon_element)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
title: SharedString,
|
||||
@@ -39,7 +44,7 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<IconName>,
|
||||
icon: Option<ModelIcon>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
@@ -60,7 +65,12 @@ impl ModelSelectorListItem {
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -105,9 +115,12 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
match icon {
|
||||
ModelIcon::Name(icon_name) => Icon::new(icon_name),
|
||||
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.child(Label::new(self.title).truncate()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
Label::new(display_text)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,7 +13,8 @@ impl ApiKeysWithProviders {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
match icon {
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(name))
|
||||
});
|
||||
div()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
has_configured_providers: bool,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ impl AgentPanelOnboarding {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
.visible_providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
|
||||
# Ask @maxdeviant about this before bumping.
|
||||
mdbook = "= 0.4.40"
|
||||
@@ -17,7 +15,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
zed.workspace = true
|
||||
zlog.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
@@ -27,4 +24,4 @@ workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "docs_preprocessor"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
zlog::init();
|
||||
zlog::init_output_stderr();
|
||||
// call a zed:: function so everything in `zed` crate is linked and
|
||||
// all actions in the actual app are registered
|
||||
zed::stdout_is_a_pty();
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
match args.get(0).map(String::as_str) {
|
||||
@@ -72,8 +69,8 @@ enum PreprocessorError {
|
||||
impl PreprocessorError {
|
||||
fn new_for_not_found_action(action_name: String) -> Self {
|
||||
for action in &*ALL_ACTIONS {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
for alias in &action.deprecated_aliases {
|
||||
if alias == action_name.as_str() {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name,
|
||||
should_be: action.name.to_string(),
|
||||
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
|
||||
chapter.content = regex
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let action = caps[1].trim();
|
||||
if find_action_by_name(action).is_none() {
|
||||
if is_missing_action(action) {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let name = caps[1].trim();
|
||||
let Some(action) = find_action_by_name(name) else {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
if actions_available() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
}
|
||||
return format!("<code class=\"hljs\">{}</code>", name);
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
})
|
||||
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
|
||||
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
|
||||
ALL_ACTIONS
|
||||
.binary_search_by(|action| action.name.cmp(name))
|
||||
.binary_search_by(|action| action.name.as_str().cmp(name))
|
||||
.ok()
|
||||
.map(|index| &ALL_ACTIONS[index])
|
||||
}
|
||||
|
||||
fn actions_available() -> bool {
|
||||
!ALL_ACTIONS.is_empty()
|
||||
}
|
||||
|
||||
fn is_missing_action(name: &str) -> bool {
|
||||
actions_available() && find_action_by_name(name).is_none()
|
||||
}
|
||||
|
||||
fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
|
||||
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
|
||||
.context("Failed to parse keymap JSON")?;
|
||||
for section in keymap.sections() {
|
||||
for (keystrokes, action) in section.bindings() {
|
||||
keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| gpui::Keystroke::parse(source))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.context("Failed to parse keystroke")?;
|
||||
for (_keystrokes, action) in section.bindings() {
|
||||
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
|
||||
.map_err(|err| anyhow::format_err!(err))
|
||||
.context("Failed to parse action")?
|
||||
{
|
||||
anyhow::ensure!(
|
||||
find_action_by_name(action_name).is_some(),
|
||||
!is_missing_action(action_name),
|
||||
"Action not found: {}",
|
||||
action_name
|
||||
);
|
||||
@@ -491,27 +493,35 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct ActionDef {
|
||||
name: &'static str,
|
||||
name: String,
|
||||
human_name: String,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
deprecated_aliases: Vec<String>,
|
||||
#[serde(rename = "documentation")]
|
||||
docs: Option<String>,
|
||||
}
|
||||
|
||||
fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
let mut actions = gpui::generate_list_of_all_registered_actions()
|
||||
.map(|action| ActionDef {
|
||||
name: action.name,
|
||||
human_name: command_palette::humanize_action_name(action.name),
|
||||
deprecated_aliases: action.deprecated_aliases,
|
||||
docs: action.documentation,
|
||||
})
|
||||
.collect::<Vec<ActionDef>>();
|
||||
|
||||
actions.sort_by_key(|a| a.name);
|
||||
|
||||
actions
|
||||
fn load_all_actions() -> Vec<ActionDef> {
|
||||
let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
|
||||
match std::fs::read_to_string(asset_path) {
|
||||
Ok(content) => {
|
||||
let mut actions: Vec<ActionDef> =
|
||||
serde_json::from_str(&content).expect("Failed to parse actions.json");
|
||||
actions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
actions
|
||||
}
|
||||
Err(err) => {
|
||||
if std::env::var("CI").is_ok() {
|
||||
panic!("actions.json not found at {}: {}", asset_path, err);
|
||||
}
|
||||
eprintln!(
|
||||
"Warning: actions.json not found, action validation will be skipped: {}",
|
||||
err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_postprocessing() -> Result<()> {
|
||||
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
actions_sorted.sort_by_key(|a| a.name.as_str());
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
|
||||
|
||||
// Add the description, escaping HTML if needed
|
||||
if let Some(description) = action.docs {
|
||||
if let Some(description) = action.docs.as_ref() {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<br>\n");
|
||||
}
|
||||
output.push_str("Keymap Name: <code>");
|
||||
output.push_str(action.name);
|
||||
output.push_str(&action.name);
|
||||
output.push_str("</code><br>\n");
|
||||
if !action.deprecated_aliases.is_empty() {
|
||||
output.push_str("Deprecated Alias(es): ");
|
||||
|
||||
@@ -893,7 +893,7 @@ impl CompletionsMenu {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.clone())
|
||||
Label::new(text.trim().to_string())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -1615,8 +1615,12 @@ impl CodeActionsMenu {
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated =
|
||||
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
|
||||
let is_truncated = line_wrapper.should_truncate_line(
|
||||
&label,
|
||||
CODE_ACTION_MENU_MAX_WIDTH,
|
||||
"…",
|
||||
gpui::TruncateFrom::End,
|
||||
);
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
|
||||
@@ -189,7 +189,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
|
||||
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _};
|
||||
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _, ToPoint as _};
|
||||
use theme::{
|
||||
AccentColors, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
|
||||
observe_buffer_font_size_adjustment,
|
||||
@@ -11413,12 +11413,25 @@ impl Editor {
|
||||
let diff = buffer.diff_for(hunk.buffer_id)?;
|
||||
let buffer = buffer.buffer(hunk.buffer_id)?;
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = diff
|
||||
.read(cx)
|
||||
.base_text()
|
||||
.as_rope()
|
||||
.slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0);
|
||||
|
||||
let base_text = diff.read(cx).base_text();
|
||||
let mut base_text_start = hunk.diff_base_byte_range.start.0.to_point(base_text);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let mut buffer_start = hunk.buffer_range.start.to_point(&buffer_snapshot);
|
||||
if base_text_start.row > 0
|
||||
&& base_text_start.column == 0
|
||||
&& buffer_start.row > 0
|
||||
&& buffer_start.column == 0
|
||||
{
|
||||
base_text_start.row -= 1;
|
||||
base_text_start.column = base_text.line_len(base_text_start.row);
|
||||
buffer_start.row -= 1;
|
||||
buffer_start.column = buffer.line_len(buffer_start.row);
|
||||
}
|
||||
|
||||
let original_text = diff.read(cx).base_text().as_rope().slice(
|
||||
text::ToOffset::to_offset(&base_text_start, base_text)..hunk.diff_base_byte_range.end.0,
|
||||
);
|
||||
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
|
||||
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
|
||||
probe
|
||||
@@ -11427,7 +11440,13 @@ impl Editor {
|
||||
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
|
||||
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
|
||||
}) {
|
||||
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
|
||||
buffer_revert_changes.insert(
|
||||
i,
|
||||
(
|
||||
buffer_snapshot.anchor_before(buffer_start)..hunk.buffer_range.end,
|
||||
original_text,
|
||||
),
|
||||
);
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -5417,6 +5417,12 @@ impl EditorElement {
|
||||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||||
);
|
||||
|
||||
// Don't show hover popovers when context menu is open to avoid overlap
|
||||
let has_context_menu = self.editor.read(cx).mouse_context_menu.is_some();
|
||||
if has_context_menu {
|
||||
return;
|
||||
}
|
||||
|
||||
let hover_popovers = self.editor.update(cx, |editor, cx| {
|
||||
editor.hover_state.render(
|
||||
snapshot,
|
||||
|
||||
@@ -205,6 +205,49 @@ impl EditorLspTestContext {
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
"#})),
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
@@ -276,6 +319,49 @@ impl EditorLspTestContext {
|
||||
(jsx_opening_element) @start
|
||||
(jsx_closing_element)? @end) @indent
|
||||
"#})),
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
@@ -19,6 +19,9 @@ impl Global for GlobalExtensionHostProxy {}
|
||||
///
|
||||
/// This object implements each of the individual proxy types so that their
|
||||
/// methods can be called directly on it.
|
||||
/// Registration function for language model providers.
|
||||
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionHostProxy {
|
||||
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
|
||||
@@ -29,6 +32,7 @@ pub struct ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
|
||||
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
|
||||
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
|
||||
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
|
||||
}
|
||||
|
||||
impl ExtensionHostProxy {
|
||||
@@ -54,6 +58,7 @@ impl ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
debug_adapter_provider_proxy: RwLock::default(),
|
||||
language_model_provider_proxy: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +95,15 @@ impl ExtensionHostProxy {
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_language_model_provider_proxy(
|
||||
&self,
|
||||
proxy: impl ExtensionLanguageModelProviderProxy,
|
||||
) {
|
||||
self.language_model_provider_proxy
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionThemeProxy: Send + Sync + 'static {
|
||||
@@ -446,3 +460,37 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
|
||||
proxy.unregister_debug_locator(locator_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App);
|
||||
}
|
||||
|
||||
impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language_model_provider(provider_id, register_fn, cx)
|
||||
}
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_language_model_provider(provider_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ pub struct ExtensionManifest {
|
||||
pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub language_model_providers: BTreeMap<Arc<str>, LanguageModelProviderManifestEntry>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
@@ -288,6 +290,16 @@ pub struct DebugAdapterManifestEntry {
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct DebugLocatorManifestEntry {}
|
||||
|
||||
/// Manifest entry for a language model provider.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageModelProviderManifestEntry {
|
||||
/// Display name for the provider.
|
||||
pub name: String,
|
||||
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
|
||||
let extension_name = extension_dir
|
||||
@@ -358,6 +370,7 @@ fn manifest_from_old_manifest(
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +404,7 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ fn manifest() -> ExtensionManifest {
|
||||
)],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -196,6 +197,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -376,6 +378,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
||||
155
crates/git_ui/src/clone.rs
Normal file
155
crates/git_ui/src/clone.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use gpui::{App, Context, WeakEntity, Window};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use std::sync::Arc;
|
||||
use ui::{Color, IconName, SharedString};
|
||||
use util::ResultExt;
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
pub fn clone_and_open(
|
||||
repo_url: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
on_success: Arc<
|
||||
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
|
||||
>,
|
||||
) {
|
||||
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let mut paths = destination_prompt.await.ok()?.ok()??;
|
||||
let mut destination_dir = paths.pop()?;
|
||||
|
||||
let repo_name = repo_url
|
||||
.split('/')
|
||||
.next_back()
|
||||
.map(|name| name.strip_suffix(".git").unwrap_or(name))
|
||||
.unwrap_or("repository")
|
||||
.to_owned();
|
||||
|
||||
let clone_task = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let destination_dir = destination_dir.clone();
|
||||
let repo_url = repo_url.clone();
|
||||
cx.spawn(async move |_workspace, _cx| {
|
||||
fs.git_clone(&repo_url, destination_dir.as_path()).await
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if let Err(error) = clone_task.await {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.log_err();
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_worktrees = workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).worktrees(cx).next().is_some()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let prompt_answer = if has_worktrees {
|
||||
cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()?
|
||||
} else {
|
||||
// Don't ask if project is empty
|
||||
0
|
||||
};
|
||||
|
||||
destination_dir.push(&repo_name);
|
||||
|
||||
match prompt_answer {
|
||||
0 => {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let create_task = workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(destination_dir.as_path(), true, cx)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let on_success = on_success.clone();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
let destination_path = destination_dir.clone();
|
||||
let on_success = on_success.clone();
|
||||
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
app_state,
|
||||
cx,
|
||||
move |workspace, window, cx| {
|
||||
cx.activate(true);
|
||||
|
||||
let create_task =
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(
|
||||
destination_path.as_path(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -2849,93 +2849,15 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let mut paths = path.await.ok()?.ok()??;
|
||||
let mut path = paths.pop()?;
|
||||
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
|
||||
|
||||
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
|
||||
|
||||
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
|
||||
Ok(_) => cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
Err(e) => {
|
||||
this.update(cx, |this: &mut GitPanel, cx| {
|
||||
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
.ok()?;
|
||||
|
||||
path.push(repo_name);
|
||||
match prompt_answer.await.ok()? {
|
||||
0 => {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(path.as_path(), true, cx)
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
workspace.app_state().clone(),
|
||||
cx,
|
||||
move |workspace, _, cx| {
|
||||
cx.activate(true);
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(&path, true, cx)
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
crate::clone::clone_and_open(
|
||||
repo.into(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -5281,7 +5203,7 @@ impl GitPanel {
|
||||
|
||||
this.child(
|
||||
self.entry_label(path_name, path_color)
|
||||
.truncate()
|
||||
.truncate_start()
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ use ui::{
|
||||
};
|
||||
|
||||
mod blame_ui;
|
||||
pub mod clone;
|
||||
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{
|
||||
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
|
||||
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
|
||||
register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
|
||||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use itertools::Itertools;
|
||||
@@ -354,7 +354,7 @@ impl TextLayout {
|
||||
None
|
||||
};
|
||||
|
||||
let (truncate_width, truncation_suffix) =
|
||||
let (truncate_width, truncation_affix, truncate_from) =
|
||||
if let Some(text_overflow) = text_style.text_overflow.clone() {
|
||||
let width = known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
|
||||
@@ -365,10 +365,11 @@ impl TextLayout {
|
||||
});
|
||||
|
||||
match text_overflow {
|
||||
TextOverflow::Truncate(s) => (width, s),
|
||||
TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
|
||||
TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
|
||||
}
|
||||
} else {
|
||||
(None, "".into())
|
||||
(None, "".into(), TruncateFrom::End)
|
||||
};
|
||||
|
||||
if let Some(text_layout) = element_state.0.borrow().as_ref()
|
||||
@@ -383,8 +384,9 @@ impl TextLayout {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_suffix,
|
||||
&truncation_affix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
} else {
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
|
||||
@@ -334,9 +334,13 @@ pub enum WhiteSpace {
|
||||
/// How to truncate text that overflows the width of the element
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum TextOverflow {
|
||||
/// Truncate the text when it doesn't fit, and represent this truncation by displaying the
|
||||
/// provided string.
|
||||
/// Truncate the text at the end when it doesn't fit, and represent this truncation by
|
||||
/// displaying the provided string (e.g., "very long te…").
|
||||
Truncate(SharedString),
|
||||
/// Truncate the text at the start when it doesn't fit, and represent this truncation by
|
||||
/// displaying the provided string at the beginning (e.g., "…ong text here").
|
||||
/// Typically more adequate for file paths where the end is more important than the beginning.
|
||||
TruncateStart(SharedString),
|
||||
}
|
||||
|
||||
/// How to align text within the element
|
||||
|
||||
@@ -75,13 +75,21 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) if needed.
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) at the end if needed.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) at the start if needed.
|
||||
/// Typically more adequate for file paths where the end is more important than the beginning.
|
||||
/// Note: This doesn't exist in Tailwind CSS.
|
||||
fn text_ellipsis_start(mut self) -> Self {
|
||||
self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text overflow behavior of the element.
|
||||
fn text_overflow(mut self, overflow: TextOverflow) -> Self {
|
||||
self.text_style().text_overflow = Some(overflow);
|
||||
|
||||
@@ -2,6 +2,15 @@ use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun,
|
||||
use collections::HashMap;
|
||||
use std::{borrow::Cow, iter, sync::Arc};
|
||||
|
||||
/// Determines whether to truncate text from the start or end.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TruncateFrom {
|
||||
/// Truncate text from the start.
|
||||
Start,
|
||||
/// Truncate text from the end.
|
||||
End,
|
||||
}
|
||||
|
||||
/// The GPUI line wrapper, used to wrap lines of text to a given width.
|
||||
pub struct LineWrapper {
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
@@ -129,29 +138,50 @@ impl LineWrapper {
|
||||
}
|
||||
|
||||
/// Determines if a line should be truncated based on its width.
|
||||
///
|
||||
/// Returns the truncation index in `line`.
|
||||
pub fn should_truncate_line(
|
||||
&mut self,
|
||||
line: &str,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
truncation_affix: &str,
|
||||
truncate_from: TruncateFrom,
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
let suffix_width = truncation_suffix
|
||||
let suffix_width = truncation_affix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut truncate_ix = 0;
|
||||
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
for (ix, c) in line.char_indices().rev() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,16 +193,23 @@ impl LineWrapper {
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
truncation_affix: &str,
|
||||
runs: &'a [TextRun],
|
||||
truncate_from: TruncateFrom,
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_suffix)
|
||||
self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
|
||||
{
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let result = match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
SharedString::from(format!("{truncation_affix}{}", &line[truncate_ix + 1..]))
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
SharedString::from(format!("{}{truncation_affix}", &line[..truncate_ix]))
|
||||
}
|
||||
};
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
@@ -245,15 +282,35 @@ impl LineWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
|
||||
fn update_runs_after_truncation(
|
||||
result: &str,
|
||||
ellipsis: &str,
|
||||
runs: &mut Vec<TextRun>,
|
||||
truncate_from: TruncateFrom,
|
||||
) {
|
||||
let mut truncate_at = result.len() - ellipsis.len();
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.truncate(run_index + 1);
|
||||
break;
|
||||
match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
for (run_index, run) in runs.iter_mut().enumerate().rev() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.splice(..run_index, std::iter::empty());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.truncate(run_index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,7 +560,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_line() {
|
||||
fn test_truncate_line_end() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -514,8 +571,13 @@ mod tests {
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
px(220.),
|
||||
ellipsis,
|
||||
&dummy_runs,
|
||||
TruncateFrom::End,
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
@@ -541,7 +603,50 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_multiple_runs() {
|
||||
fn test_truncate_line_start() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
expected: &'static str,
|
||||
ellipsis: &str,
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
px(220.),
|
||||
ellipsis,
|
||||
&dummy_runs,
|
||||
TruncateFrom::Start,
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"cccc ddddd eeee fff gg",
|
||||
"",
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"…ccc ddddd eeee fff gg",
|
||||
"…",
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"......dddd eeee fff gg",
|
||||
"......",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_multiple_runs_end() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -554,7 +659,7 @@ mod tests {
|
||||
) {
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
@@ -600,10 +705,75 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_run_after_truncation() {
|
||||
fn test_truncate_multiple_runs_start() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
#[track_caller]
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
expected: &str,
|
||||
run_lens: &[usize],
|
||||
result_run_len: &[usize],
|
||||
line_width: Pixels,
|
||||
) {
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
line_width,
|
||||
"…",
|
||||
&dummy_runs,
|
||||
TruncateFrom::Start,
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
}
|
||||
// Case 0: Normal
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: …ijkl (truncate_at = 9)
|
||||
// Run res: Run0 { string: …ijkl, len: 7, ... }
|
||||
perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: …ghijkl (truncate_at = 7)
|
||||
// Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
|
||||
// 4, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"…ghijkl",
|
||||
&[4, 4, 4],
|
||||
&[5, 4],
|
||||
px(70.),
|
||||
);
|
||||
// Case 2: Truncate at start of some run
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 3)
|
||||
// Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: ijkl, len: 4, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"…efghijkl",
|
||||
&[4, 4, 4],
|
||||
&[3, 4, 4],
|
||||
px(90.),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_run_after_truncation_end() {
|
||||
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
|
||||
@@ -1141,6 +1141,104 @@ fn test_text_objects(cx: &mut App) {
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_text_objects_with_has_parent_predicate(cx: &mut App) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Create a language with a custom text_objects query that uses #has-parent?
|
||||
// This query only matches closure_expression when it's inside a call_expression
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
; Only match closures that are arguments to function calls
|
||||
(closure_expression) @function.around
|
||||
(#has-parent? @function.around arguments)
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
let (text, ranges) = marked_text_ranges(
|
||||
indoc! {r#"
|
||||
fn main() {
|
||||
let standalone = |x| x + 1;
|
||||
let result = foo(|y| y * ˇ2);
|
||||
}"#
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let matches = snapshot
|
||||
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
|
||||
.map(|(range, text_object)| (&text[range], text_object))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Should only match the closure inside foo(), not the standalone closure
|
||||
assert_eq!(matches, &[("|y| y * 2", TextObject::AroundFunction),]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_text_objects_with_not_has_parent_predicate(cx: &mut App) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Create a language with a custom text_objects query that uses #not-has-parent?
|
||||
// This query only matches closure_expression when it's NOT inside a call_expression
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
; Only match closures that are NOT arguments to function calls
|
||||
(closure_expression) @function.around
|
||||
(#not-has-parent? @function.around arguments)
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
let (text, ranges) = marked_text_ranges(
|
||||
indoc! {r#"
|
||||
fn main() {
|
||||
let standalone = |x| x +ˇ 1;
|
||||
let result = foo(|y| y * 2);
|
||||
}"#
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let matches = snapshot
|
||||
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
|
||||
.map(|(range, text_object)| (&text[range], text_object))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Should only match the standalone closure, not the one inside foo()
|
||||
assert_eq!(matches, &[("|x| x + 1", TextObject::AroundFunction),]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut App) {
|
||||
#[track_caller]
|
||||
|
||||
@@ -19,7 +19,10 @@ use std::{
|
||||
use streaming_iterator::StreamingIterator;
|
||||
use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
|
||||
use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
|
||||
use tree_sitter::{
|
||||
Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatch, QueryMatches,
|
||||
QueryPredicateArg, Tree,
|
||||
};
|
||||
|
||||
pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
|
||||
|
||||
@@ -82,6 +85,7 @@ struct SyntaxMapMatchesLayer<'a> {
|
||||
next_captures: Vec<QueryCapture<'a>>,
|
||||
has_next: bool,
|
||||
matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
|
||||
query: &'a Query,
|
||||
grammar_index: usize,
|
||||
_query_cursor: QueryCursorHandle,
|
||||
}
|
||||
@@ -1163,6 +1167,7 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||
depth: layer.depth,
|
||||
grammar_index,
|
||||
matches,
|
||||
query,
|
||||
next_pattern_index: 0,
|
||||
next_captures: Vec::new(),
|
||||
has_next: false,
|
||||
@@ -1260,13 +1265,20 @@ impl SyntaxMapCapturesLayer<'_> {
|
||||
|
||||
impl SyntaxMapMatchesLayer<'_> {
|
||||
fn advance(&mut self) {
|
||||
if let Some(mat) = self.matches.next() {
|
||||
self.next_captures.clear();
|
||||
self.next_captures.extend_from_slice(mat.captures);
|
||||
self.next_pattern_index = mat.pattern_index;
|
||||
self.has_next = true;
|
||||
} else {
|
||||
self.has_next = false;
|
||||
loop {
|
||||
if let Some(mat) = self.matches.next() {
|
||||
if !satisfies_custom_predicates(self.query, mat) {
|
||||
continue;
|
||||
}
|
||||
self.next_captures.clear();
|
||||
self.next_captures.extend_from_slice(mat.captures);
|
||||
self.next_pattern_index = mat.pattern_index;
|
||||
self.has_next = true;
|
||||
return;
|
||||
} else {
|
||||
self.has_next = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,6 +1307,39 @@ impl<'a> Iterator for SyntaxMapCaptures<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn satisfies_custom_predicates(query: &Query, mat: &QueryMatch) -> bool {
|
||||
for predicate in query.general_predicates(mat.pattern_index) {
|
||||
let satisfied = match predicate.operator.as_ref() {
|
||||
"has-parent?" => has_parent(&predicate.args, mat),
|
||||
"not-has-parent?" => !has_parent(&predicate.args, mat),
|
||||
_ => true,
|
||||
};
|
||||
if !satisfied {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn has_parent(args: &[QueryPredicateArg], mat: &QueryMatch) -> bool {
|
||||
let (
|
||||
Some(QueryPredicateArg::Capture(capture_ix)),
|
||||
Some(QueryPredicateArg::String(parent_kind)),
|
||||
) = (args.first(), args.get(1))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(capture) = mat.captures.iter().find(|c| c.index == *capture_ix) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
capture
|
||||
.node
|
||||
.parent()
|
||||
.is_some_and(|p| p.kind() == parent_kind.as_ref())
|
||||
}
|
||||
|
||||
fn join_ranges(
|
||||
a: impl Iterator<Item = Range<usize>>,
|
||||
b: impl Iterator<Item = Range<usize>>,
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
//! which is a set of tools used to interact with the projects written in said language.
|
||||
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
|
||||
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
@@ -36,7 +39,7 @@ pub struct Toolchain {
|
||||
/// - Only in the subproject they're currently in.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum ToolchainScope {
|
||||
Subproject(WorktreeId, Arc<RelPath>),
|
||||
Subproject(Arc<Path>, Arc<RelPath>),
|
||||
Project,
|
||||
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
|
||||
Global,
|
||||
|
||||
@@ -797,11 +797,26 @@ pub enum AuthenticateError {
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Either a built-in icon name or a path to an external SVG.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IconOrSvg {
|
||||
/// A built-in icon from Zed's icon set.
|
||||
Icon(IconName),
|
||||
/// Path to a custom SVG icon file.
|
||||
Svg(SharedString),
|
||||
}
|
||||
|
||||
impl Default for IconOrSvg {
|
||||
fn default() -> Self {
|
||||
Self::Icon(IconName::ZedAssistant)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelProvider: 'static {
|
||||
fn id(&self) -> LanguageModelProviderId;
|
||||
fn name(&self) -> LanguageModelProviderName;
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ZedAssistant
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::default()
|
||||
}
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
|
||||
@@ -820,7 +835,7 @@ pub trait LanguageModelProvider: 'static {
|
||||
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
pub enum ConfigurationViewTargetAgent {
|
||||
#[default]
|
||||
ZedAgent,
|
||||
|
||||
@@ -2,12 +2,16 @@ use crate::{
|
||||
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderState,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
|
||||
/// Function type for checking if a built-in provider should be hidden.
|
||||
/// Returns Some(extension_id) if the provider should be hidden when that extension is installed.
|
||||
pub type BuiltinProviderHidingFn = Box<dyn Fn(&str) -> Option<&'static str> + Send + Sync>;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| LanguageModelRegistry::default());
|
||||
cx.set_global(GlobalLanguageModelRegistry(registry));
|
||||
@@ -48,6 +52,11 @@ pub struct LanguageModelRegistry {
|
||||
thread_summary_model: Option<ConfiguredModel>,
|
||||
providers: BTreeMap<LanguageModelProviderId, Arc<dyn LanguageModelProvider>>,
|
||||
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
|
||||
/// Set of installed extension IDs that provide language models.
|
||||
/// Used to determine which built-in providers should be hidden.
|
||||
installed_llm_extension_ids: HashSet<Arc<str>>,
|
||||
/// Function to check if a built-in provider should be hidden by an extension.
|
||||
builtin_provider_hiding_fn: Option<BuiltinProviderHidingFn>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -104,6 +113,8 @@ pub enum Event {
|
||||
ProviderStateChanged(LanguageModelProviderId),
|
||||
AddedProvider(LanguageModelProviderId),
|
||||
RemovedProvider(LanguageModelProviderId),
|
||||
/// Emitted when provider visibility changes due to extension install/uninstall.
|
||||
ProvidersChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for LanguageModelRegistry {}
|
||||
@@ -183,6 +194,60 @@ impl LanguageModelRegistry {
|
||||
providers
|
||||
}
|
||||
|
||||
/// Returns providers, filtering out hidden built-in providers.
|
||||
pub fn visible_providers(&self) -> Vec<Arc<dyn LanguageModelProvider>> {
|
||||
self.providers()
|
||||
.into_iter()
|
||||
.filter(|p| !self.should_hide_provider(&p.id()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sets the function used to check if a built-in provider should be hidden.
|
||||
pub fn set_builtin_provider_hiding_fn(&mut self, hiding_fn: BuiltinProviderHidingFn) {
|
||||
self.builtin_provider_hiding_fn = Some(hiding_fn);
|
||||
}
|
||||
|
||||
/// Called when an extension is installed/loaded.
|
||||
/// If the extension provides language models, track it so we can hide the corresponding built-in.
|
||||
pub fn extension_installed(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
|
||||
if self.installed_llm_extension_ids.insert(extension_id) {
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when an extension is uninstalled/unloaded.
|
||||
pub fn extension_uninstalled(&mut self, extension_id: &str, cx: &mut Context<Self>) {
|
||||
if self.installed_llm_extension_ids.remove(extension_id) {
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the set of installed LLM extension IDs.
|
||||
pub fn sync_installed_llm_extensions(
|
||||
&mut self,
|
||||
extension_ids: HashSet<Arc<str>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if extension_ids != self.installed_llm_extension_ids {
|
||||
self.installed_llm_extension_ids = extension_ids;
|
||||
cx.emit(Event::ProvidersChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a provider should be hidden from the UI.
|
||||
/// Built-in providers are hidden when their corresponding extension is installed.
|
||||
pub fn should_hide_provider(&self, provider_id: &LanguageModelProviderId) -> bool {
|
||||
if let Some(ref hiding_fn) = self.builtin_provider_hiding_fn {
|
||||
if let Some(extension_id) = hiding_fn(&provider_id.0) {
|
||||
return self.installed_llm_extension_ids.contains(extension_id);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn configuration_error(
|
||||
&self,
|
||||
model: Option<ConfiguredModel>,
|
||||
@@ -416,4 +481,132 @@ mod tests {
|
||||
let providers = registry.read(cx).providers();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_provider_hiding_on_extension_install(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let provider_id = provider.id();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert_eq!(visible.len(), 1);
|
||||
assert_eq!(visible[0].id(), provider_id);
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_installed("fake-extension".into(), cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert!(visible.is_empty());
|
||||
|
||||
let all = registry.read(cx).providers();
|
||||
assert_eq!(all.len(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_provider_unhiding_on_extension_uninstall(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
let provider_id = provider.id();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
registry.extension_installed("fake-extension".into(), cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert!(visible.is_empty());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_uninstalled("fake-extension", cx);
|
||||
});
|
||||
|
||||
let visible = registry.read(cx).visible_providers();
|
||||
assert_eq!(visible.len(), 1);
|
||||
assert_eq!(visible[0].id(), provider_id);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_should_hide_provider(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "anthropic" {
|
||||
Some("anthropic")
|
||||
} else if id == "openai" {
|
||||
Some("openai")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
registry.extension_installed("anthropic".into(), cx);
|
||||
});
|
||||
|
||||
let registry_read = registry.read(cx);
|
||||
|
||||
assert!(registry_read.should_hide_provider(&LanguageModelProviderId("anthropic".into())));
|
||||
|
||||
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("openai".into())));
|
||||
|
||||
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("unknown".into())));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_sync_installed_llm_extensions(cx: &mut App) {
|
||||
let registry = cx.new(|_| LanguageModelRegistry::default());
|
||||
|
||||
let provider = Arc::new(FakeLanguageModelProvider::default());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.register_provider(provider.clone(), cx);
|
||||
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
|
||||
if id == "fake" {
|
||||
Some("fake-extension")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let mut extension_ids = HashSet::default();
|
||||
extension_ids.insert(Arc::from("fake-extension"));
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(extension_ids, cx);
|
||||
});
|
||||
|
||||
assert!(registry.read(cx).visible_providers().is_empty());
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(HashSet::default(), cx);
|
||||
});
|
||||
|
||||
assert_eq!(registry.read(cx).visible_providers().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ convert_case.workspace = true
|
||||
copilot.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
|
||||
67
crates/language_models/src/extension.rs
Normal file
67
crates/language_models/src/extension.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use collections::HashMap;
|
||||
use extension::{
|
||||
ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration,
|
||||
};
|
||||
use gpui::{App, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
/// Maps built-in provider IDs to their corresponding extension IDs.
|
||||
/// When an extension with this ID is installed, the built-in provider should be hidden.
|
||||
static BUILTIN_TO_EXTENSION_MAP: LazyLock<HashMap<&'static str, &'static str>> =
|
||||
LazyLock::new(|| {
|
||||
let mut map = HashMap::default();
|
||||
map.insert("anthropic", "anthropic");
|
||||
map.insert("openai", "openai");
|
||||
map.insert("google", "google-ai");
|
||||
map.insert("openrouter", "openrouter");
|
||||
map.insert("copilot_chat", "copilot-chat");
|
||||
map
|
||||
});
|
||||
|
||||
/// Returns the extension ID that should hide the given built-in provider.
|
||||
pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> {
|
||||
BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied()
|
||||
}
|
||||
|
||||
/// Proxy that registers extension language model providers with the LanguageModelRegistry.
|
||||
pub struct LanguageModelProviderRegistryProxy {
|
||||
registry: Entity<LanguageModelRegistry>,
|
||||
}
|
||||
|
||||
impl LanguageModelProviderRegistryProxy {
|
||||
pub fn new(registry: Entity<LanguageModelRegistry>) -> Self {
|
||||
Self { registry }
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
_provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
register_fn(cx);
|
||||
}
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
|
||||
self.registry.update(cx, |registry, cx| {
|
||||
registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the extension language model provider proxy.
|
||||
/// This must be called BEFORE extension_host::init to ensure the proxy is available
|
||||
/// when extensions try to register their language model providers.
|
||||
pub fn init_proxy(cx: &mut App) {
|
||||
let proxy = ExtensionHostProxy::default_global(cx);
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
registry.update(cx, |registry, _cx| {
|
||||
registry.set_builtin_provider_hiding_fn(Box::new(extension_for_builtin_provider));
|
||||
});
|
||||
|
||||
proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry));
|
||||
}
|
||||
@@ -7,9 +7,12 @@ use gpui::{App, Context, Entity};
|
||||
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
|
||||
pub mod extension;
|
||||
pub mod provider;
|
||||
mod settings;
|
||||
|
||||
pub use crate::extension::init_proxy as init_extension_proxy;
|
||||
|
||||
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
||||
use crate::provider::cloud::CloudLanguageModelProvider;
|
||||
@@ -31,6 +34,56 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
register_language_model_providers(registry, user_store, client.clone(), cx);
|
||||
});
|
||||
|
||||
// Subscribe to extension store events to track LLM extension installations
|
||||
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
|
||||
cx.subscribe(&extension_store, {
|
||||
let registry = registry.clone();
|
||||
move |extension_store, event, cx| match event {
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => {
|
||||
if let Some(manifest) = extension_store
|
||||
.read(cx)
|
||||
.extension_manifest_for_id(extension_id)
|
||||
{
|
||||
if !manifest.language_model_providers.is_empty() {
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_installed(extension_id.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
extension_host::Event::ExtensionUninstalled(extension_id) => {
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.extension_uninstalled(extension_id, cx);
|
||||
});
|
||||
}
|
||||
extension_host::Event::ExtensionsUpdated => {
|
||||
let mut new_ids = HashSet::default();
|
||||
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
|
||||
if !entry.manifest.language_model_providers.is_empty() {
|
||||
new_ids.insert(extension_id.clone());
|
||||
}
|
||||
}
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.sync_installed_llm_extensions(new_ids, cx);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Initialize with currently installed extensions
|
||||
registry.update(cx, |registry, cx| {
|
||||
let mut initial_ids = HashSet::default();
|
||||
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
|
||||
if !entry.manifest.language_model_providers.is_empty() {
|
||||
initial_ids.insert(extension_id.clone());
|
||||
}
|
||||
}
|
||||
registry.sync_installed_llm_extensions(initial_ids, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
|
||||
.openai_compatible
|
||||
.keys()
|
||||
|
||||
@@ -8,7 +8,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
|
||||
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
|
||||
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
@@ -125,8 +125,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiAnthropic
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiAnthropic)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -30,7 +30,7 @@ use gpui::{
|
||||
use gpui_tokio::Tokio;
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
AuthenticateError, EnvVar, LanguageModel, LanguageModelCacheConfiguration,
|
||||
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
@@ -426,8 +426,8 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiBedrock
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiBedrock)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -19,7 +19,7 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Ta
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
|
||||
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||
@@ -304,8 +304,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiZed
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiZed)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -18,12 +18,12 @@ use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
|
||||
use http_client::StatusCode;
|
||||
use language::language_settings::all_language_settings;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
|
||||
StopReason, TokenUsage,
|
||||
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
|
||||
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
|
||||
MessageContent, RateLimiter, Role, StopReason, TokenUsage,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::prelude::*;
|
||||
@@ -104,8 +104,8 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Copilot
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::Copilot)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -127,8 +127,8 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiDeepSeek
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiDeepSeek)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
@@ -164,8 +164,8 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiGoogle
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiGoogle)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -10,7 +10,7 @@ use language_model::{
|
||||
StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, RateLimiter, Role,
|
||||
};
|
||||
@@ -175,8 +175,8 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiLmStudio
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiLmStudio)
|
||||
}
|
||||
|
||||
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -176,8 +176,8 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiMistral
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiMistral)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{Stream, TryFutureExt, stream};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
|
||||
@@ -221,8 +221,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOllama
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOllama)
|
||||
}
|
||||
|
||||
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
@@ -249,33 +249,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
|
||||
}
|
||||
|
||||
// Override with available models from settings
|
||||
for setting_model in &OllamaLanguageModelProvider::settings(cx).available_models {
|
||||
let setting_base = setting_model.name.split(':').next().unwrap();
|
||||
if let Some(model) = models
|
||||
.values_mut()
|
||||
.find(|m| m.name.split(':').next().unwrap() == setting_base)
|
||||
{
|
||||
model.max_tokens = setting_model.max_tokens;
|
||||
model.display_name = setting_model.display_name.clone();
|
||||
model.keep_alive = setting_model.keep_alive.clone();
|
||||
model.supports_tools = setting_model.supports_tools;
|
||||
model.supports_vision = setting_model.supports_images;
|
||||
model.supports_thinking = setting_model.supports_thinking;
|
||||
} else {
|
||||
models.insert(
|
||||
setting_model.name.clone(),
|
||||
ollama::Model {
|
||||
name: setting_model.name.clone(),
|
||||
display_name: setting_model.display_name.clone(),
|
||||
max_tokens: setting_model.max_tokens,
|
||||
keep_alive: setting_model.keep_alive.clone(),
|
||||
supports_tools: setting_model.supports_tools,
|
||||
supports_vision: setting_model.supports_images,
|
||||
supports_thinking: setting_model.supports_thinking,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
merge_settings_into_models(&mut models, &settings.available_models);
|
||||
|
||||
let mut models = models
|
||||
.into_values()
|
||||
@@ -921,6 +895,35 @@ impl Render for ConfigurationView {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_settings_into_models(
|
||||
models: &mut HashMap<String, ollama::Model>,
|
||||
available_models: &[AvailableModel],
|
||||
) {
|
||||
for setting_model in available_models {
|
||||
if let Some(model) = models.get_mut(&setting_model.name) {
|
||||
model.max_tokens = setting_model.max_tokens;
|
||||
model.display_name = setting_model.display_name.clone();
|
||||
model.keep_alive = setting_model.keep_alive.clone();
|
||||
model.supports_tools = setting_model.supports_tools;
|
||||
model.supports_vision = setting_model.supports_images;
|
||||
model.supports_thinking = setting_model.supports_thinking;
|
||||
} else {
|
||||
models.insert(
|
||||
setting_model.name.clone(),
|
||||
ollama::Model {
|
||||
name: setting_model.name.clone(),
|
||||
display_name: setting_model.display_name.clone(),
|
||||
max_tokens: setting_model.max_tokens,
|
||||
keep_alive: setting_model.keep_alive.clone(),
|
||||
supports_tools: setting_model.supports_tools,
|
||||
supports_vision: setting_model.supports_images,
|
||||
supports_thinking: setting_model.supports_thinking,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
|
||||
ollama::OllamaTool::Function {
|
||||
function: OllamaFunctionTool {
|
||||
@@ -930,3 +933,83 @@ fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_merge_settings_preserves_display_names_for_similar_models() {
|
||||
// Regression test for https://github.com/zed-industries/zed/issues/43646
|
||||
// When multiple models share the same base name (e.g., qwen2.5-coder:1.5b and qwen2.5-coder:3b),
|
||||
// each model should get its own display_name from settings, not a random one.
|
||||
|
||||
let mut models: HashMap<String, ollama::Model> = HashMap::new();
|
||||
models.insert(
|
||||
"qwen2.5-coder:1.5b".to_string(),
|
||||
ollama::Model {
|
||||
name: "qwen2.5-coder:1.5b".to_string(),
|
||||
display_name: None,
|
||||
max_tokens: 4096,
|
||||
keep_alive: None,
|
||||
supports_tools: None,
|
||||
supports_vision: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
);
|
||||
models.insert(
|
||||
"qwen2.5-coder:3b".to_string(),
|
||||
ollama::Model {
|
||||
name: "qwen2.5-coder:3b".to_string(),
|
||||
display_name: None,
|
||||
max_tokens: 4096,
|
||||
keep_alive: None,
|
||||
supports_tools: None,
|
||||
supports_vision: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
);
|
||||
|
||||
let available_models = vec![
|
||||
AvailableModel {
|
||||
name: "qwen2.5-coder:1.5b".to_string(),
|
||||
display_name: Some("QWEN2.5 Coder 1.5B".to_string()),
|
||||
max_tokens: 5000,
|
||||
keep_alive: None,
|
||||
supports_tools: Some(true),
|
||||
supports_images: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
AvailableModel {
|
||||
name: "qwen2.5-coder:3b".to_string(),
|
||||
display_name: Some("QWEN2.5 Coder 3B".to_string()),
|
||||
max_tokens: 6000,
|
||||
keep_alive: None,
|
||||
supports_tools: Some(true),
|
||||
supports_images: None,
|
||||
supports_thinking: None,
|
||||
},
|
||||
];
|
||||
|
||||
merge_settings_into_models(&mut models, &available_models);
|
||||
|
||||
let model_1_5b = models
|
||||
.get("qwen2.5-coder:1.5b")
|
||||
.expect("1.5b model missing");
|
||||
let model_3b = models.get("qwen2.5-coder:3b").expect("3b model missing");
|
||||
|
||||
assert_eq!(
|
||||
model_1_5b.display_name,
|
||||
Some("QWEN2.5 Coder 1.5B".to_string()),
|
||||
"1.5b model should have its own display_name"
|
||||
);
|
||||
assert_eq!(model_1_5b.max_tokens, 5000);
|
||||
|
||||
assert_eq!(
|
||||
model_3b.display_name,
|
||||
Some("QWEN2.5 Coder 3B".to_string()),
|
||||
"3b model should have its own display_name"
|
||||
);
|
||||
assert_eq!(model_3b.max_tokens, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -122,8 +122,8 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenAi
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenAi)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
@@ -133,8 +133,8 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenAiCompat
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenAiCompat)
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
|
||||
@@ -180,8 +180,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiOpenRouter
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiOpenRouter)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
|
||||
@@ -117,8 +117,8 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiVZero
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiVZero)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
|
||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
@@ -118,8 +118,8 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
|
||||
PROVIDER_NAME
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::AiXAi
|
||||
fn icon(&self) -> IconOrSvg {
|
||||
IconOrSvg::Icon(IconName::AiXAi)
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
|
||||
|
||||
@@ -127,6 +127,16 @@ impl LanguageServerState {
|
||||
return menu;
|
||||
};
|
||||
|
||||
let server_versions = self
|
||||
.lsp_store
|
||||
.update(cx, |lsp_store, _| {
|
||||
lsp_store
|
||||
.language_server_statuses()
|
||||
.map(|(server_id, status)| (server_id, status.server_version.clone()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut first_button_encountered = false;
|
||||
for item in &self.items {
|
||||
if let LspMenuItem::ToggleServersButton { restart } = item {
|
||||
@@ -254,6 +264,22 @@ impl LanguageServerState {
|
||||
};
|
||||
|
||||
let server_name = server_info.name.clone();
|
||||
let server_version = server_versions
|
||||
.get(&server_info.id)
|
||||
.and_then(|version| version.clone());
|
||||
|
||||
let tooltip_text = match (&server_version, &message) {
|
||||
(None, None) => None,
|
||||
(Some(version), None) => {
|
||||
Some(SharedString::from(format!("Version: {}", version.as_ref())))
|
||||
}
|
||||
(None, Some(message)) => Some(message.clone()),
|
||||
(Some(version), Some(message)) => Some(SharedString::from(format!(
|
||||
"Version: {}\n\n{}",
|
||||
version.as_ref(),
|
||||
message.as_ref()
|
||||
))),
|
||||
};
|
||||
menu = menu.item(ContextMenuItem::custom_entry(
|
||||
move |_, _| {
|
||||
h_flex()
|
||||
@@ -355,11 +381,11 @@ impl LanguageServerState {
|
||||
}
|
||||
}
|
||||
},
|
||||
message.map(|server_message| {
|
||||
tooltip_text.map(|tooltip_text| {
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Right,
|
||||
DocumentationEdge::Bottom,
|
||||
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
|
||||
DocumentationEdge::Top,
|
||||
Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
|
||||
)
|
||||
}),
|
||||
));
|
||||
|
||||
@@ -330,6 +330,8 @@ impl LspLogView {
|
||||
let server_info = format!(
|
||||
"* Server: {NAME} (id {ID})
|
||||
|
||||
* Version: {VERSION}
|
||||
|
||||
* Binary: {BINARY}
|
||||
|
||||
* Registered workspace folders:
|
||||
@@ -340,6 +342,12 @@ impl LspLogView {
|
||||
* Configuration: {CONFIGURATION}",
|
||||
NAME = info.status.name,
|
||||
ID = info.id,
|
||||
VERSION = info
|
||||
.status
|
||||
.server_version
|
||||
.as_ref()
|
||||
.map(|version| version.as_ref())
|
||||
.unwrap_or("Unknown"),
|
||||
BINARY = info
|
||||
.status
|
||||
.binary
|
||||
@@ -1334,6 +1342,7 @@ impl ServerInfo {
|
||||
capabilities: server.capabilities(),
|
||||
status: LanguageServerStatus {
|
||||
name: server.name(),
|
||||
server_version: server.version(),
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
|
||||
@@ -18,13 +18,47 @@
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
((arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
|
||||
(arrow_function) @function.around
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
; Arrow function in variable declaration (captures body for expression-bodied arrows)
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function
|
||||
body: (_) @function.inside) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
|
||||
(generator_function
|
||||
body: (_
|
||||
|
||||
@@ -18,13 +18,47 @@
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
((arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
|
||||
(arrow_function) @function.around
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
; Arrow function in variable declaration (expression body fallback)
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function
|
||||
body: (_) @function.inside) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
(function_signature) @function.around
|
||||
|
||||
(generator_function
|
||||
|
||||
@@ -18,13 +18,48 @@
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
((arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
|
||||
(arrow_function) @function.around
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
; Arrow function in variable declaration - capture body as @function.inside
|
||||
; (for statement blocks, the more specific pattern above captures just the contents)
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (_) @function.inside)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function
|
||||
body: (_) @function.inside) @function.around
|
||||
(#not-has-parent? @function.around variable_declarator))
|
||||
(function_signature) @function.around
|
||||
|
||||
(generator_function
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "YAML"
|
||||
grammar = "yaml"
|
||||
path_suffixes = ["yml", "yaml", "pixi.lock", "clang-format", "clangd"]
|
||||
path_suffixes = ["yml", "yaml", "pixi.lock", "clang-format", "clangd", "bst"]
|
||||
line_comments = ["# "]
|
||||
autoclose_before = ",]}"
|
||||
brackets = [
|
||||
|
||||
@@ -89,6 +89,7 @@ pub struct LanguageServer {
|
||||
outbound_tx: channel::Sender<String>,
|
||||
notification_tx: channel::Sender<NotificationSerializer>,
|
||||
name: LanguageServerName,
|
||||
version: Option<SharedString>,
|
||||
process_name: Arc<str>,
|
||||
binary: LanguageServerBinary,
|
||||
capabilities: RwLock<ServerCapabilities>,
|
||||
@@ -501,6 +502,7 @@ impl LanguageServer {
|
||||
response_handlers,
|
||||
io_handlers,
|
||||
name: server_name,
|
||||
version: None,
|
||||
process_name: binary
|
||||
.path
|
||||
.file_name()
|
||||
@@ -882,7 +884,9 @@ impl LanguageServer {
|
||||
window: Some(WindowClientCapabilities {
|
||||
work_done_progress: Some(true),
|
||||
show_message: Some(ShowMessageRequestClientCapabilities {
|
||||
message_action_item: None,
|
||||
message_action_item: Some(MessageActionItemCapabilities {
|
||||
additional_properties_support: Some(true),
|
||||
}),
|
||||
}),
|
||||
..WindowClientCapabilities::default()
|
||||
}),
|
||||
@@ -923,6 +927,7 @@ impl LanguageServer {
|
||||
)
|
||||
})?;
|
||||
if let Some(info) = response.server_info {
|
||||
self.version = info.version.map(SharedString::from);
|
||||
self.process_name = info.name.into();
|
||||
}
|
||||
self.capabilities = RwLock::new(response.capabilities);
|
||||
@@ -1153,6 +1158,11 @@ impl LanguageServer {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
/// Get the version of the running language server.
|
||||
pub fn version(&self) -> Option<SharedString> {
|
||||
self.version.clone()
|
||||
}
|
||||
|
||||
pub fn process_name(&self) -> &str {
|
||||
&self.process_name
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
extension.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -128,6 +128,7 @@ use util::{
|
||||
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
|
||||
paths::{PathStyle, SanitizedPath},
|
||||
post_inc,
|
||||
redact::redact_command,
|
||||
rel_path::RelPath,
|
||||
};
|
||||
|
||||
@@ -577,9 +578,12 @@ impl LocalLspStore {
|
||||
},
|
||||
},
|
||||
);
|
||||
log::error!("Failed to start language server {server_name:?}: {err:?}");
|
||||
log::error!(
|
||||
"Failed to start language server {server_name:?}: {}",
|
||||
redact_command(&format!("{err:?}"))
|
||||
);
|
||||
if !log.is_empty() {
|
||||
log::error!("server stderr: {log}");
|
||||
log::error!("server stderr: {}", redact_command(&log));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -3860,6 +3864,7 @@ pub enum LspStoreEvent {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LanguageServerStatus {
|
||||
pub name: LanguageServerName,
|
||||
pub server_version: Option<SharedString>,
|
||||
pub pending_work: BTreeMap<ProgressToken, LanguageServerProgress>,
|
||||
pub has_pending_diagnostic_updates: bool,
|
||||
pub progress_tokens: HashSet<ProgressToken>,
|
||||
@@ -8350,6 +8355,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name,
|
||||
server_version: None,
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
@@ -9385,6 +9391,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name: server_name.clone(),
|
||||
server_version: None,
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
@@ -11415,6 +11422,7 @@ impl LspStore {
|
||||
server_id,
|
||||
LanguageServerStatus {
|
||||
name: language_server.name(),
|
||||
server_version: language_server.version(),
|
||||
pending_work: Default::default(),
|
||||
has_pending_diagnostic_updates: false,
|
||||
progress_tokens: Default::default(),
|
||||
@@ -13768,7 +13776,7 @@ impl From<lsp::Documentation> for CompletionDocumentation {
|
||||
match docs {
|
||||
lsp::Documentation::String(text) => {
|
||||
if text.lines().count() <= 1 {
|
||||
CompletionDocumentation::SingleLine(text.into())
|
||||
CompletionDocumentation::SingleLine(text.trim().to_string().into())
|
||||
} else {
|
||||
CompletionDocumentation::MultiLinePlainText(text.into())
|
||||
}
|
||||
@@ -14360,4 +14368,22 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_newline_in_completion_documentation() {
|
||||
let doc = lsp::Documentation::String(
|
||||
"Inappropriate argument value (of correct type).\n".to_string(),
|
||||
);
|
||||
let completion_doc: CompletionDocumentation = doc.into();
|
||||
assert!(
|
||||
matches!(completion_doc, CompletionDocumentation::SingleLine(s) if s == "Inappropriate argument value (of correct type).")
|
||||
);
|
||||
|
||||
let doc = lsp::Documentation::String(" some value \n".to_string());
|
||||
let completion_doc: CompletionDocumentation = doc.into();
|
||||
assert!(matches!(
|
||||
completion_doc,
|
||||
CompletionDocumentation::SingleLine(s) if s == "some value"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ use debugger::{
|
||||
dap_store::{DapStore, DapStoreEvent},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
use encoding_rs;
|
||||
pub use environment::ProjectEnvironment;
|
||||
#[cfg(test)]
|
||||
use futures::future::join_all;
|
||||
@@ -1330,7 +1330,12 @@ impl Project {
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||
.detach();
|
||||
let toolchain_store = cx.new(|cx| {
|
||||
ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
|
||||
ToolchainStore::remote(
|
||||
REMOTE_SERVER_PROJECT_ID,
|
||||
worktree_store.clone(),
|
||||
remote.read(cx).proto_client(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let task_store = cx.new(|cx| {
|
||||
TaskStore::remote(
|
||||
@@ -5444,6 +5449,48 @@ impl Project {
|
||||
worktree.read(cx).entry_for_path(rel_path).is_some()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_local_settings_file(
|
||||
&self,
|
||||
worktree_id: WorktreeId,
|
||||
rel_path: Arc<RelPath>,
|
||||
cx: &mut App,
|
||||
update: impl 'static + Send + FnOnce(&mut settings::SettingsContent, &App),
|
||||
) {
|
||||
let Some(worktree) = self.worktree_for_id(worktree_id, cx) else {
|
||||
// todo(settings_ui) error?
|
||||
return;
|
||||
};
|
||||
cx.spawn(async move |cx| {
|
||||
let file = worktree
|
||||
.update(cx, |worktree, cx| worktree.load_file(&rel_path, cx))?
|
||||
.await
|
||||
.context("Failed to load settings file")?;
|
||||
|
||||
let has_bom = file.has_bom;
|
||||
|
||||
let new_text = cx.read_global::<SettingsStore, _>(|store, cx| {
|
||||
store.new_text_for_update(file.text, move |settings| update(settings, cx))
|
||||
})?;
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
let line_ending = text::LineEnding::detect(&new_text);
|
||||
worktree.write_file(
|
||||
rel_path.clone(),
|
||||
new_text.into(),
|
||||
line_ending,
|
||||
encoding_rs::UTF_8,
|
||||
has_bom,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("Failed to write settings file")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathMatchCandidateSet {
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::{
|
||||
pub struct ToolchainStore {
|
||||
mode: ToolchainStoreInner,
|
||||
user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
_sub: Subscription,
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ impl ToolchainStore {
|
||||
) -> Self {
|
||||
let entity = cx.new(|_| LocalToolchainStore {
|
||||
languages,
|
||||
worktree_store,
|
||||
worktree_store: worktree_store.clone(),
|
||||
project_environment,
|
||||
active_toolchains: Default::default(),
|
||||
manifest_tree,
|
||||
@@ -77,12 +78,18 @@ impl ToolchainStore {
|
||||
});
|
||||
Self {
|
||||
mode: ToolchainStoreInner::Local(entity),
|
||||
worktree_store,
|
||||
user_toolchains: Default::default(),
|
||||
_sub,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) -> Self {
|
||||
pub(super) fn remote(
|
||||
project_id: u64,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
client: AnyProtoClient,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let entity = cx.new(|_| RemoteToolchainStore { client, project_id });
|
||||
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
|
||||
cx.emit(e.clone())
|
||||
@@ -90,6 +97,7 @@ impl ToolchainStore {
|
||||
Self {
|
||||
mode: ToolchainStoreInner::Remote(entity),
|
||||
user_toolchains: Default::default(),
|
||||
worktree_store,
|
||||
_sub,
|
||||
}
|
||||
}
|
||||
@@ -165,12 +173,22 @@ impl ToolchainStore {
|
||||
language_name: LanguageName,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Toolchains>> {
|
||||
let Some(worktree) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let target_root_path = worktree.read_with(cx, |this, _| this.abs_path());
|
||||
|
||||
let user_toolchains = self
|
||||
.user_toolchains
|
||||
.iter()
|
||||
.filter(|(scope, _)| {
|
||||
if let ToolchainScope::Subproject(worktree_id, relative_path) = scope {
|
||||
path.worktree_id == *worktree_id && relative_path.starts_with(&path.path)
|
||||
if let ToolchainScope::Subproject(subproject_root_path, relative_path) = scope {
|
||||
target_root_path == *subproject_root_path
|
||||
&& relative_path.starts_with(&path.path)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
1
crates/project/src/x.py
Normal file
1
crates/project/src/x.py
Normal file
@@ -0,0 +1 @@
|
||||
Gliwice makerspace
|
||||
@@ -45,6 +45,7 @@ workspace.workspace = true
|
||||
language.workspace = true
|
||||
zed_actions.workspace = true
|
||||
telemetry.workspace = true
|
||||
notifications.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -29,6 +29,7 @@ use gpui::{
|
||||
};
|
||||
use language::DiagnosticSeverity;
|
||||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
|
||||
ProjectPath, Worktree, WorktreeId,
|
||||
@@ -1140,6 +1141,12 @@ impl ProjectPanel {
|
||||
"Copy Relative Path",
|
||||
Box::new(zed_actions::workspace::CopyRelativePath),
|
||||
)
|
||||
.when(!is_dir && self.has_git_changes(entry_id), |menu| {
|
||||
menu.separator().action(
|
||||
"Restore File",
|
||||
Box::new(git::RestoreFile { skip_prompt: false }),
|
||||
)
|
||||
})
|
||||
.when(has_git_repo, |menu| {
|
||||
menu.separator()
|
||||
.action("View File History", Box::new(git::FileHistory))
|
||||
@@ -1180,6 +1187,19 @@ impl ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool {
|
||||
for visible in &self.state.visible_entries {
|
||||
if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) {
|
||||
let total_modified =
|
||||
git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified;
|
||||
let total_deleted =
|
||||
git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted;
|
||||
return total_modified > 0 || total_deleted > 0;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
|
||||
if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) {
|
||||
return false;
|
||||
@@ -2041,6 +2061,100 @@ impl ProjectPanel {
|
||||
self.remove(false, action.skip_prompt, window, cx);
|
||||
}
|
||||
|
||||
fn restore_file(
|
||||
&mut self,
|
||||
action: &git::RestoreFile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let selection = self.state.selection?;
|
||||
let project = self.project.read(cx);
|
||||
|
||||
let (_worktree, entry) = self.selected_sub_entry(cx)?;
|
||||
if entry.is_dir() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let project_path = project.path_for_entry(selection.entry_id, cx)?;
|
||||
|
||||
let git_store = project.git_store();
|
||||
let (repository, repo_path) = git_store
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)?;
|
||||
|
||||
let snapshot = repository.read(cx).snapshot();
|
||||
let status = snapshot.status_for_path(&repo_path)?;
|
||||
if !status.status.is_modified() && !status.status.is_deleted() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_name = entry.path.file_name()?.to_string();
|
||||
|
||||
let answer = if !action.skip_prompt {
|
||||
let prompt = format!("Discard changes to {}?", file_name);
|
||||
Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
if let Some(answer) = answer
|
||||
&& answer.await != Ok(0)
|
||||
{
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
let task = panel.update(cx, |_panel, cx| {
|
||||
repository.update(cx, |repo, cx| {
|
||||
repo.checkout_files("HEAD", vec![repo_path], cx)
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Err(e) = task.await {
|
||||
panel
|
||||
.update(cx, |panel, cx| {
|
||||
let message = format!("Failed to restore {}: {}", file_name, e);
|
||||
let toast = StatusToast::new(message, cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
panel
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.project.update(cx, |project, cx| {
|
||||
if let Some(buffer_id) = project
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.buffer_id_for_project_path(&project_path)
|
||||
{
|
||||
if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.reload(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn remove(
|
||||
&mut self,
|
||||
trash: bool,
|
||||
@@ -5631,6 +5745,7 @@ impl Render for ProjectPanel {
|
||||
.on_action(cx.listener(Self::copy))
|
||||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::duplicate))
|
||||
.on_action(cx.listener(Self::restore_file))
|
||||
.when(!project.is_remote(), |el| {
|
||||
el.on_action(cx.listener(Self::trash))
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heck.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -17,7 +17,7 @@ where
|
||||
labels: &'static [&'static str],
|
||||
should_do_title_case: bool,
|
||||
tab_index: Option<isize>,
|
||||
on_change: Rc<dyn Fn(T, &mut ui::Window, &mut App) + 'static>,
|
||||
on_change: Rc<dyn Fn(T, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl<T> EnumVariantDropdown<T>
|
||||
@@ -29,7 +29,7 @@ where
|
||||
current_value: T,
|
||||
variants: &'static [T],
|
||||
labels: &'static [&'static str],
|
||||
on_change: impl Fn(T, &mut ui::Window, &mut App) + 'static,
|
||||
on_change: impl Fn(T, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
@@ -78,8 +78,8 @@ where
|
||||
value == current_value,
|
||||
IconPosition::End,
|
||||
None,
|
||||
move |window, cx| {
|
||||
on_change(value, window, cx);
|
||||
move |_, cx| {
|
||||
on_change(value, cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct FontPickerDelegate {
|
||||
filtered_fonts: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_font: SharedString,
|
||||
on_font_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl FontPickerDelegate {
|
||||
fn new(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> Self {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
@@ -132,10 +132,10 @@ impl PickerDelegate for FontPickerDelegate {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
|
||||
if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
|
||||
let font = font_match.string.clone();
|
||||
(self.on_font_changed)(font.into(), window, cx);
|
||||
(self.on_font_changed)(font.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ impl PickerDelegate for FontPickerDelegate {
|
||||
|
||||
pub fn font_picker(
|
||||
current_font: SharedString,
|
||||
on_font_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<FontPicker>,
|
||||
) -> FontPicker {
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct IconThemePickerDelegate {
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl IconThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
@@ -32,15 +32,15 @@ impl IconThemePickerDelegate {
|
||||
|
||||
let selected_index = icon_themes
|
||||
.iter()
|
||||
.position(|icon_theme| *icon_theme == current_theme)
|
||||
.position(|icon_themes| *icon_themes == current_theme)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_themes = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, theme)| StringMatch {
|
||||
.map(|(index, icon_themes)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
string: icon_themes.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -67,18 +67,13 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
index: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) {
|
||||
self.selected_index = index.min(self.filtered_themes.len().saturating_sub(1));
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<IconThemePicker>) {
|
||||
self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search icon themes…".into()
|
||||
"Search icon theme…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
@@ -94,9 +89,9 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, theme)| StringMatch {
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
string: icon_theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -105,16 +100,16 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
let _candidates: Vec<StringMatchCandidate> = icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref()))
|
||||
.map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref()))
|
||||
.collect();
|
||||
|
||||
icon_themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, theme)| StringMatch {
|
||||
.filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase()))
|
||||
.map(|(index, icon_theme)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: theme.to_string(),
|
||||
string: icon_theme.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
@@ -124,7 +119,7 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
let selected_index = if query.is_empty() {
|
||||
icon_themes
|
||||
.iter()
|
||||
.position(|theme| *theme == current_theme)
|
||||
.position(|icon_theme| *icon_theme == current_theme)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
matches
|
||||
@@ -143,12 +138,12 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), window, cx);
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,15 +156,15 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
index: usize,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<IconThemePicker>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let theme_match = self.filtered_themes.get(index)?;
|
||||
let theme_match = self.filtered_themes.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(index)
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
@@ -181,7 +176,7 @@ impl PickerDelegate for IconThemePickerDelegate {
|
||||
|
||||
pub fn icon_theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<IconThemePicker>,
|
||||
) -> IconThemePicker {
|
||||
|
||||
@@ -9,7 +9,7 @@ use ui::{
|
||||
pub struct SettingsInputField {
|
||||
initial_text: Option<String>,
|
||||
placeholder: Option<&'static str>,
|
||||
confirm: Option<Box<dyn Fn(Option<String>, &mut Window, &mut App)>>,
|
||||
confirm: Option<Box<dyn Fn(Option<String>, &mut App)>>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
@@ -34,10 +34,7 @@ impl SettingsInputField {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_confirm(
|
||||
mut self,
|
||||
confirm: impl Fn(Option<String>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
pub fn on_confirm(mut self, confirm: impl Fn(Option<String>, &mut App) + 'static) -> Self {
|
||||
self.confirm = Some(Box::new(confirm));
|
||||
self
|
||||
}
|
||||
@@ -86,13 +83,13 @@ impl RenderOnce for SettingsInputField {
|
||||
.child(editor)
|
||||
.when_some(self.confirm, |this, confirm| {
|
||||
this.on_action::<menu::Confirm>({
|
||||
move |_, window, cx| {
|
||||
move |_, _, cx| {
|
||||
let Some(editor) = weak_editor.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let new_value = editor.read_with(cx, |editor, cx| editor.text(cx));
|
||||
let new_value = (!new_value.is_empty()).then_some(new_value);
|
||||
confirm(new_value, window, cx);
|
||||
confirm(new_value, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,13 +13,13 @@ pub struct ThemePickerDelegate {
|
||||
filtered_themes: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut Window, &mut App) + 'static>,
|
||||
on_theme_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl ThemePickerDelegate {
|
||||
fn new(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> Self {
|
||||
let theme_registry = ThemeRegistry::global(cx);
|
||||
@@ -130,10 +130,10 @@ impl PickerDelegate for ThemePickerDelegate {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<ThemePicker>) {
|
||||
if let Some(theme_match) = self.filtered_themes.get(self.selected_index) {
|
||||
let theme = theme_match.string.clone();
|
||||
(self.on_theme_changed)(theme.into(), window, cx);
|
||||
(self.on_theme_changed)(theme.into(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ impl PickerDelegate for ThemePickerDelegate {
|
||||
|
||||
pub fn theme_picker(
|
||||
current_theme: SharedString,
|
||||
on_theme_changed: impl Fn(SharedString, &mut Window, &mut App) + 'static,
|
||||
on_theme_changed: impl Fn(SharedString, &mut App) + 'static,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ThemePicker>,
|
||||
) -> ThemePicker {
|
||||
|
||||
@@ -217,7 +217,7 @@ fn render_api_key_provider(
|
||||
SettingsInputField::new()
|
||||
.tab_index(0)
|
||||
.with_placeholder("xxxxxxxxxxxxxxxxxxxx")
|
||||
.on_confirm(move |api_key, _window, cx| {
|
||||
.on_confirm(move |api_key, cx| {
|
||||
write_key(api_key.filter(|key| !key.is_empty()), cx);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ mod components;
|
||||
mod page_data;
|
||||
mod pages;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -11,9 +11,7 @@ use gpui::{
|
||||
Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list,
|
||||
};
|
||||
|
||||
use language::Buffer;
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use project::{Project, WorktreeId};
|
||||
use release_channel::ReleaseChannel;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -156,7 +154,7 @@ trait AnySettingField {
|
||||
current_file: &SettingsUiFile,
|
||||
file_set_in: &settings::SettingsFile,
|
||||
cx: &App,
|
||||
) -> Option<Box<dyn Fn(&mut Window, &mut App)>>;
|
||||
) -> Option<Box<dyn Fn(&mut App)>>;
|
||||
|
||||
fn json_path(&self) -> Option<&'static str>;
|
||||
}
|
||||
@@ -186,7 +184,7 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
current_file: &SettingsUiFile,
|
||||
file_set_in: &settings::SettingsFile,
|
||||
cx: &App,
|
||||
) -> Option<Box<dyn Fn(&mut Window, &mut App)>> {
|
||||
) -> Option<Box<dyn Fn(&mut App)>> {
|
||||
if file_set_in == &settings::SettingsFile::Default {
|
||||
return None;
|
||||
}
|
||||
@@ -205,7 +203,7 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
}
|
||||
let current_file = current_file.clone();
|
||||
|
||||
return Some(Box::new(move |window, cx| {
|
||||
return Some(Box::new(move |cx| {
|
||||
let store = SettingsStore::global(cx);
|
||||
let default_value = (this.pick)(store.raw_default_settings());
|
||||
let is_set_somewhere_other_than_default = store
|
||||
@@ -217,15 +215,9 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||
} else {
|
||||
None
|
||||
};
|
||||
update_settings_file(
|
||||
current_file.clone(),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _| {
|
||||
(this.write)(settings, value_to_set);
|
||||
},
|
||||
)
|
||||
update_settings_file(current_file.clone(), None, cx, move |settings, _| {
|
||||
(this.write)(settings, value_to_set);
|
||||
})
|
||||
// todo(settings_ui): Don't log err
|
||||
.log_err();
|
||||
}));
|
||||
@@ -583,6 +575,7 @@ pub fn open_settings_editor(
|
||||
}
|
||||
|
||||
// We have to defer this to get the workspace off the stack.
|
||||
|
||||
let path = path.map(ToOwned::to_owned);
|
||||
cx.defer(move |cx| {
|
||||
let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
|
||||
@@ -678,8 +671,6 @@ pub struct SettingsWindow {
|
||||
pages: Vec<SettingsPage>,
|
||||
search_bar: Entity<Editor>,
|
||||
search_task: Option<Task<()>>,
|
||||
/// Cached settings file buffers to avoid repeated disk I/O on each settings change
|
||||
project_setting_file_buffers: HashMap<ProjectPath, Entity<Buffer>>,
|
||||
/// Index into navbar_entries
|
||||
navbar_entry: usize,
|
||||
navbar_entries: Vec<NavBarEntry>,
|
||||
@@ -1078,8 +1069,8 @@ fn render_settings_item(
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Reset to Default"))
|
||||
.on_click({
|
||||
move |_, window, cx| {
|
||||
reset_to_default(window, cx);
|
||||
move |_, _, cx| {
|
||||
reset_to_default(cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -1508,7 +1499,6 @@ impl SettingsWindow {
|
||||
files: vec![],
|
||||
|
||||
current_file: current_file,
|
||||
project_setting_file_buffers: HashMap::default(),
|
||||
pages: vec![],
|
||||
navbar_entries: vec![],
|
||||
navbar_entry: 0,
|
||||
@@ -2036,9 +2026,14 @@ impl SettingsWindow {
|
||||
}
|
||||
|
||||
if let Some(worktree_id) = settings_ui_file.worktree_id() {
|
||||
let directory_name = all_projects(self.original_window.as_ref(), cx)
|
||||
let directory_name = all_projects(cx)
|
||||
.find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx))
|
||||
.map(|worktree| worktree.read(cx).root_name());
|
||||
.and_then(|worktree| worktree.read(cx).root_dir())
|
||||
.and_then(|root_dir| {
|
||||
root_dir
|
||||
.file_name()
|
||||
.map(|os_string| os_string.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
let Some(directory_name) = directory_name else {
|
||||
log::error!(
|
||||
@@ -2048,8 +2043,7 @@ impl SettingsWindow {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.worktree_root_dirs
|
||||
.insert(worktree_id, directory_name.as_unix_str().to_string());
|
||||
self.worktree_root_dirs.insert(worktree_id, directory_name);
|
||||
}
|
||||
|
||||
let focus_handle = prev_files
|
||||
@@ -2065,7 +2059,7 @@ impl SettingsWindow {
|
||||
|
||||
let mut missing_worktrees = Vec::new();
|
||||
|
||||
for worktree in all_projects(self.original_window.as_ref(), cx)
|
||||
for worktree in all_projects(cx)
|
||||
.flat_map(|project| project.read(cx).visible_worktrees(cx))
|
||||
.filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id()))
|
||||
{
|
||||
@@ -3531,10 +3525,7 @@ impl Render for SettingsWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn all_projects(
|
||||
window: Option<&WindowHandle<Workspace>>,
|
||||
cx: &App,
|
||||
) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
|
||||
workspace::AppState::global(cx)
|
||||
.upgrade()
|
||||
.map(|app_state| {
|
||||
@@ -3544,9 +3535,6 @@ fn all_projects(
|
||||
.workspaces()
|
||||
.iter()
|
||||
.filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone()))
|
||||
.chain(
|
||||
window.and_then(|workspace| Some(workspace.read(cx).ok()?.project().clone())),
|
||||
)
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -3555,7 +3543,6 @@ fn all_projects(
|
||||
fn update_settings_file(
|
||||
file: SettingsUiFile,
|
||||
file_name: Option<&'static str>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
||||
) -> Result<()> {
|
||||
@@ -3564,11 +3551,41 @@ fn update_settings_file(
|
||||
match file {
|
||||
SettingsUiFile::Project((worktree_id, rel_path)) => {
|
||||
let rel_path = rel_path.join(paths::local_settings_file_relative_path());
|
||||
let Some(settings_window) = window.root::<SettingsWindow>().flatten() else {
|
||||
anyhow::bail!("No settings window found");
|
||||
let Some((worktree, project)) = all_projects(cx).find_map(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(Some(project))
|
||||
}) else {
|
||||
anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
|
||||
};
|
||||
|
||||
update_project_setting_file(worktree_id, rel_path, update, settings_window, cx)
|
||||
project.update(cx, |project, cx| {
|
||||
let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) {
|
||||
None
|
||||
} else {
|
||||
Some(worktree.update(cx, |worktree, cx| {
|
||||
worktree.create_entry(rel_path.clone(), false, None, cx)
|
||||
}))
|
||||
};
|
||||
|
||||
cx.spawn(async move |project, cx| {
|
||||
if let Some(task) = task
|
||||
&& task.await.is_err()
|
||||
{
|
||||
return;
|
||||
};
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.update_local_settings_file(worktree_id, rel_path, cx, update);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
SettingsUiFile::User => {
|
||||
// todo(settings_ui) error?
|
||||
@@ -3579,86 +3596,6 @@ fn update_settings_file(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_project_setting_file(
|
||||
worktree_id: WorktreeId,
|
||||
rel_path: Arc<RelPath>,
|
||||
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
|
||||
settings_window: Entity<SettingsWindow>,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
let Some((worktree, project)) =
|
||||
all_projects(settings_window.read(cx).original_window.as_ref(), cx).find_map(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(Some(project))
|
||||
})
|
||||
else {
|
||||
anyhow::bail!("Could not find project with worktree id: {}", worktree_id);
|
||||
};
|
||||
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path.clone(),
|
||||
};
|
||||
|
||||
let needs_creation = !project
|
||||
.read(cx)
|
||||
.contains_local_settings_file(worktree_id, &rel_path, cx);
|
||||
|
||||
let create_task = needs_creation.then(|| {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.create_entry(rel_path.clone(), false, None, cx)
|
||||
})
|
||||
});
|
||||
let buffer_store = project.read(cx).buffer_store().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(create_task) = create_task {
|
||||
create_task.await?;
|
||||
}
|
||||
let cached_buffer = settings_window.read_with(cx, |settings_window, _| {
|
||||
settings_window
|
||||
.project_setting_file_buffers
|
||||
.get(&project_path)
|
||||
.cloned()
|
||||
})?;
|
||||
let buffer = if let Some(cached_buffer) = cached_buffer {
|
||||
cached_buffer
|
||||
} else {
|
||||
let buffer = buffer_store
|
||||
.update(cx, |store, cx| store.open_buffer(project_path.clone(), cx))?
|
||||
.await
|
||||
.context("Failed to open settings file")?;
|
||||
|
||||
settings_window.update(cx, |this, _cx| {
|
||||
this.project_setting_file_buffers
|
||||
.insert(project_path, buffer.clone());
|
||||
})?;
|
||||
|
||||
buffer
|
||||
};
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let current_text = buffer.text();
|
||||
let new_text = cx
|
||||
.global::<SettingsStore>()
|
||||
.new_text_for_update(current_text, |settings| update(settings, cx));
|
||||
buffer.edit([(0..buffer.len(), new_text)], None, cx);
|
||||
})?;
|
||||
|
||||
buffer_store
|
||||
.update(cx, |store, cx| store.save_buffer(buffer, cx))?
|
||||
.await
|
||||
.context("Failed to save settings file")?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
||||
field: SettingField<T>,
|
||||
file: SettingsUiFile,
|
||||
@@ -3680,16 +3617,10 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
||||
|editor, placeholder| editor.with_placeholder(placeholder),
|
||||
)
|
||||
.on_confirm({
|
||||
move |new_text, window, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, new_text.map(Into::into));
|
||||
},
|
||||
)
|
||||
move |new_text, cx| {
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, new_text.map(Into::into));
|
||||
})
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3714,11 +3645,11 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
|
||||
Switch::new("toggle_button", toggle_state)
|
||||
.tab_index(0_isize)
|
||||
.on_click({
|
||||
move |state, window, cx| {
|
||||
move |state, _window, cx| {
|
||||
telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
|
||||
|
||||
let state = *state == ui::ToggleState::Selected;
|
||||
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, _cx| {
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(state.into()));
|
||||
})
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
@@ -3738,17 +3669,11 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
|
||||
let value = value.copied().unwrap_or_else(T::min_value);
|
||||
NumberField::new("numeric_stepper", value, window, cx)
|
||||
.on_change({
|
||||
move |value, window, cx| {
|
||||
move |value, _window, cx| {
|
||||
let value = *value;
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
},
|
||||
)
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
})
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3776,19 +3701,13 @@ where
|
||||
let current_value = current_value.copied().unwrap_or(variants()[0]);
|
||||
|
||||
EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), {
|
||||
move |value, window, cx| {
|
||||
move |value, cx| {
|
||||
if value == current_value {
|
||||
return;
|
||||
}
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
},
|
||||
)
|
||||
update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
|
||||
(field.write)(settings, Some(value));
|
||||
})
|
||||
.log_err(); // todo(settings_ui) don't log err
|
||||
}
|
||||
})
|
||||
@@ -3833,11 +3752,10 @@ fn render_font_picker(
|
||||
Some(cx.new(move |cx| {
|
||||
font_picker(
|
||||
current_value.clone().into(),
|
||||
move |font_name, window, cx| {
|
||||
move |font_name, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(settings, Some(font_name.into()));
|
||||
@@ -3883,11 +3801,10 @@ fn render_theme_picker(
|
||||
let current_value = current_value.clone();
|
||||
theme_picker(
|
||||
current_value,
|
||||
move |theme_name, window, cx| {
|
||||
move |theme_name, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(
|
||||
@@ -3936,11 +3853,10 @@ fn render_icon_theme_picker(
|
||||
let current_value = current_value.clone();
|
||||
icon_theme_picker(
|
||||
current_value,
|
||||
move |theme_name, window, cx| {
|
||||
move |theme_name, cx| {
|
||||
update_settings_file(
|
||||
file.clone(),
|
||||
field.json_path,
|
||||
window,
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
(field.write)(
|
||||
@@ -4054,7 +3970,6 @@ pub mod test {
|
||||
worktree_root_dirs: HashMap::default(),
|
||||
files: Vec::default(),
|
||||
current_file: crate::SettingsUiFile::User,
|
||||
project_setting_file_buffers: HashMap::default(),
|
||||
pages,
|
||||
search_bar: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
|
||||
|
||||
@@ -939,7 +939,6 @@ impl TerminalPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<WeakEntity<Terminal>>> {
|
||||
let reveal = spawn_task.reveal;
|
||||
let reveal_target = spawn_task.reveal_target;
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn_in(window, async move |terminal_panel, cx| {
|
||||
let project = terminal_panel.update(cx, |this, cx| {
|
||||
@@ -955,6 +954,14 @@ impl TerminalPanel {
|
||||
terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
|
||||
})?;
|
||||
|
||||
let reveal_target = terminal_panel.update(cx, |panel, _| {
|
||||
if panel.center.panes().iter().any(|p| **p == task_pane) {
|
||||
RevealTarget::Dock
|
||||
} else {
|
||||
RevealTarget::Center
|
||||
}
|
||||
})?;
|
||||
|
||||
match reveal {
|
||||
RevealStrategy::Always => match reveal_target {
|
||||
RevealTarget::Center => {
|
||||
|
||||
@@ -50,28 +50,24 @@ impl ScrollableHandle for TerminalScrollHandle {
|
||||
let state = self.state.borrow();
|
||||
size(
|
||||
Pixels::ZERO,
|
||||
state
|
||||
.total_lines
|
||||
.checked_sub(state.viewport_lines)
|
||||
.unwrap_or(0) as f32
|
||||
* state.line_height,
|
||||
state.total_lines.saturating_sub(state.viewport_lines) as f32 * state.line_height,
|
||||
)
|
||||
}
|
||||
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
let state = self.state.borrow();
|
||||
let scroll_offset = state.total_lines - state.viewport_lines - state.display_offset;
|
||||
Point::new(
|
||||
Pixels::ZERO,
|
||||
-(scroll_offset as f32 * self.state.borrow().line_height),
|
||||
)
|
||||
let scroll_offset = state
|
||||
.total_lines
|
||||
.saturating_sub(state.viewport_lines)
|
||||
.saturating_sub(state.display_offset);
|
||||
Point::new(Pixels::ZERO, -(scroll_offset as f32 * state.line_height))
|
||||
}
|
||||
|
||||
fn set_offset(&self, point: Point<Pixels>) {
|
||||
let state = self.state.borrow();
|
||||
let offset_delta = (point.y / state.line_height).round() as i32;
|
||||
|
||||
let max_offset = state.total_lines - state.viewport_lines;
|
||||
let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
|
||||
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
|
||||
|
||||
self.future_display_offset
|
||||
|
||||
28
crates/title_bar/build.rs
Normal file
28
crates/title_bar/build.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rustc-check-cfg=cfg(macos_sdk_26)");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("xcrun")
|
||||
.args(["--sdk", "macosx", "--show-sdk-version"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let sdk_version = String::from_utf8(output.stdout).unwrap();
|
||||
let major_version: Option<u32> = sdk_version
|
||||
.trim()
|
||||
.split('.')
|
||||
.next()
|
||||
.and_then(|v| v.parse().ok());
|
||||
|
||||
if let Some(major) = major_version
|
||||
&& major >= 26
|
||||
{
|
||||
println!("cargo:rustc-cfg=macos_sdk_26");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/// Use pixels here instead of a rem-based size because the macOS traffic
|
||||
/// lights are a static size, and don't scale with the rest of the UI.
|
||||
///
|
||||
/// Magic number: There is one extra pixel of padding on the left side due to
|
||||
/// the 1px border around the window on macOS apps.
|
||||
// Use pixels here instead of a rem-based size because the macOS traffic
|
||||
// lights are a static size, and don't scale with the rest of the UI.
|
||||
//
|
||||
// Magic number: There is one extra pixel of padding on the left side due to
|
||||
// the 1px border around the window on macOS apps.
|
||||
#[cfg(macos_sdk_26)]
|
||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 78.;
|
||||
|
||||
#[cfg(not(macos_sdk_26))]
|
||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;
|
||||
|
||||
@@ -447,34 +447,38 @@ impl TitleBar {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Button::new("restricted_mode_trigger", "Restricted Mode")
|
||||
.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Warning)
|
||||
.icon(IconName::Warning)
|
||||
.icon_color(Color::Warning)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(|_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"You're in Restricted Mode",
|
||||
Some(&ToggleWorktreeSecurity),
|
||||
"Mark this project as trusted and unlock all features",
|
||||
cx,
|
||||
)
|
||||
let button = Button::new("restricted_mode_trigger", "Restricted Mode")
|
||||
.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Warning)
|
||||
.icon(IconName::Warning)
|
||||
.icon_color(Color::Warning)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(|_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"You're in Restricted Mode",
|
||||
Some(&ToggleWorktreeSecurity),
|
||||
"Mark this project as trusted and unlock all features",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
});
|
||||
|
||||
if cfg!(macos_sdk_26) {
|
||||
// Make up for Tahoe's traffic light buttons having less spacing around them
|
||||
Some(div().child(button).ml_0p5().into_any_element())
|
||||
} else {
|
||||
Some(button.into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
|
||||
@@ -198,10 +198,17 @@ impl ActiveToolchain {
|
||||
.or_else(|| toolchains.toolchains.first())
|
||||
.cloned();
|
||||
if let Some(toolchain) = &default_choice {
|
||||
let worktree_root_path = project
|
||||
.read_with(cx, |this, cx| {
|
||||
this.worktree_for_id(worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
workspace::WORKSPACE_DB
|
||||
.set_toolchain(
|
||||
workspace_id,
|
||||
worktree_id,
|
||||
worktree_root_path,
|
||||
relative_path.clone(),
|
||||
toolchain.clone(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod active_toolchain;
|
||||
|
||||
pub use active_toolchain::ActiveToolchain;
|
||||
use anyhow::Context as _;
|
||||
use convert_case::Casing as _;
|
||||
use editor::Editor;
|
||||
use file_finder::OpenPathDelegate;
|
||||
@@ -62,6 +63,7 @@ struct AddToolchainState {
|
||||
language_name: LanguageName,
|
||||
root_path: ProjectPath,
|
||||
weak: WeakEntity<ToolchainSelector>,
|
||||
worktree_root_path: Arc<Path>,
|
||||
}
|
||||
|
||||
struct ScopePickerState {
|
||||
@@ -99,12 +101,17 @@ impl AddToolchainState {
|
||||
root_path: ProjectPath,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ToolchainSelector>,
|
||||
) -> Entity<Self> {
|
||||
) -> anyhow::Result<Entity<Self>> {
|
||||
let weak = cx.weak_entity();
|
||||
|
||||
cx.new(|cx| {
|
||||
let worktree_root_path = project
|
||||
.read(cx)
|
||||
.worktree_for_id(root_path.worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.context("Could not find worktree")?;
|
||||
Ok(cx.new(|cx| {
|
||||
let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx));
|
||||
|
||||
Self {
|
||||
state: AddState::Path {
|
||||
_subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
|
||||
@@ -118,8 +125,9 @@ impl AddToolchainState {
|
||||
language_name,
|
||||
root_path,
|
||||
weak,
|
||||
worktree_root_path,
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_path_browser_delegate(
|
||||
@@ -237,7 +245,15 @@ impl AddToolchainState {
|
||||
// Suggest a default scope based on the applicability.
|
||||
let scope = if let Some(project_path) = resolved_toolchain_path {
|
||||
if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
|
||||
ToolchainScope::Subproject(root_path.worktree_id, root_path.path)
|
||||
let worktree_root_path = project
|
||||
.read_with(cx, |this, cx| {
|
||||
this.worktree_for_id(root_path.worktree_id, cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.context("Could not find a worktree with a given worktree ID")?;
|
||||
ToolchainScope::Subproject(worktree_root_path, root_path.path)
|
||||
} else {
|
||||
ToolchainScope::Project
|
||||
}
|
||||
@@ -400,7 +416,7 @@ impl Render for AddToolchainState {
|
||||
ToolchainScope::Global,
|
||||
ToolchainScope::Project,
|
||||
ToolchainScope::Subproject(
|
||||
self.root_path.worktree_id,
|
||||
self.worktree_root_path.clone(),
|
||||
self.root_path.path.clone(),
|
||||
),
|
||||
];
|
||||
@@ -693,7 +709,7 @@ impl ToolchainSelector {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(self.state, State::Search(_)) {
|
||||
self.state = State::AddToolchain(AddToolchainState::new(
|
||||
let Ok(state) = AddToolchainState::new(
|
||||
self.project.clone(),
|
||||
self.language_name.clone(),
|
||||
ProjectPath {
|
||||
@@ -702,7 +718,10 @@ impl ToolchainSelector {
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.state = State::AddToolchain(state);
|
||||
self.state.focus_handle(cx).focus(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -899,11 +918,17 @@ impl PickerDelegate for ToolchainSelectorDelegate {
|
||||
{
|
||||
let workspace = self.workspace.clone();
|
||||
let worktree_id = self.worktree_id;
|
||||
let worktree_abs_path_root = self.worktree_abs_path_root.clone();
|
||||
let path = self.relative_path.clone();
|
||||
let relative_path = self.relative_path.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
workspace::WORKSPACE_DB
|
||||
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
|
||||
.set_toolchain(
|
||||
workspace_id,
|
||||
worktree_abs_path_root,
|
||||
relative_path,
|
||||
toolchain.clone(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
workspace
|
||||
|
||||
@@ -893,39 +893,57 @@ impl ContextMenu {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
documentation_aside,
|
||||
..
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.entity().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
let keep_open_on_confirm = self.keep_open_on_confirm;
|
||||
move |_, window, cx| {
|
||||
handler(context.as_ref(), window, cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
|
||||
if keep_open_on_confirm {
|
||||
menu.rebuild(window, cx);
|
||||
} else {
|
||||
cx.emit(DismissEvent);
|
||||
div()
|
||||
.id(("context-menu-child", ix))
|
||||
.when_some(documentation_aside.clone(), |this, documentation_aside| {
|
||||
this.occlude()
|
||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.documentation_aside = Some((ix, documentation_aside.clone()));
|
||||
} else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
|
||||
{
|
||||
menu.documentation_aside = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
let keep_open_on_confirm = self.keep_open_on_confirm;
|
||||
move |_, window, cx| {
|
||||
handler(context.as_ref(), window, cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
|
||||
if keep_open_on_confirm {
|
||||
menu.rebuild(window, cx);
|
||||
} else {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(window, cx))
|
||||
})
|
||||
.child(entry_render(window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,17 +126,6 @@ enum IconSource {
|
||||
ExternalSvg(SharedString),
|
||||
}
|
||||
|
||||
impl IconSource {
|
||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
if path.starts_with("icons/") {
|
||||
Self::Embedded(path)
|
||||
} else {
|
||||
Self::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct Icon {
|
||||
source: IconSource,
|
||||
@@ -155,9 +144,18 @@ impl Icon {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
|
||||
/// - Paths starting with "icons/" are treated as embedded SVGs
|
||||
/// - Other paths are treated as external raster images (from icon themes)
|
||||
pub fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
let source = if path.starts_with("icons/") {
|
||||
IconSource::Embedded(path)
|
||||
} else {
|
||||
IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
};
|
||||
Self {
|
||||
source: IconSource::from_path(path),
|
||||
source,
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user