Compare commits
32 Commits
git-integr
...
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
|
||||
|
||||
16
Cargo.lock
generated
16
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",
|
||||
@@ -12571,6 +12570,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -20265,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
|
||||
|
||||
|
||||
@@ -1319,17 +1319,15 @@
|
||||
// Globs to match files that will be considered "hidden". These files can be hidden from the
|
||||
// project panel by toggling the "hide_hidden" setting.
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git integration settings.
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Master switch to disable all git integration features.
|
||||
// When true, all git features are disabled regardless of other settings.
|
||||
// When false (default), individual features are controlled by their respective settings.
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to show git status indicators (modified, added, deleted) in the
|
||||
// project panel, outline panel, and tabs.
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to show git diff information, including gutter diff indicators
|
||||
// and scrollbar diff markers.
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,7 +64,10 @@ use project::{
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
project_settings::ProjectSettings,
|
||||
};
|
||||
use settings::{GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, Settings};
|
||||
use settings::{
|
||||
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||||
Settings,
|
||||
};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -2201,8 +2204,8 @@ impl EditorElement {
|
||||
.display_diff_hunks_for_rows(display_rows, folded_buffers)
|
||||
.map(|hunk| (hunk, None))
|
||||
.collect::<Vec<_>>();
|
||||
let git_settings = &ProjectSettings::get_global(cx).git;
|
||||
if git_settings.is_gutter_enabled() {
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
|
||||
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
|
||||
for (hunk, hitbox) in &mut display_hunks {
|
||||
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
|
||||
let hunk_bounds =
|
||||
@@ -5414,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,
|
||||
@@ -6466,7 +6475,12 @@ impl EditorElement {
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| ProjectSettings::get_global(cx).git.is_gutter_enabled());
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
GitGutterSetting::TrackedFiles
|
||||
)
|
||||
});
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -365,27 +365,6 @@ pub struct GitSettings {
|
||||
pub path_style: GitPathStyle,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
/// Returns whether git status indicators should be shown.
|
||||
/// This includes status in the project panel, outline panel, and tabs.
|
||||
pub fn is_status_enabled(&self) -> bool {
|
||||
self.enabled.status
|
||||
}
|
||||
|
||||
/// Returns whether git diff features should be shown.
|
||||
/// This includes gutter diff indicators and scrollbar diff markers.
|
||||
pub fn is_diff_enabled(&self) -> bool {
|
||||
self.enabled.diff
|
||||
}
|
||||
|
||||
/// Returns whether the git gutter should be shown.
|
||||
/// This checks both the global diff setting and the gutter-specific setting.
|
||||
pub fn is_gutter_enabled(&self) -> bool {
|
||||
self.is_diff_enabled()
|
||||
&& matches!(self.git_gutter, settings::GitGutterSetting::TrackedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Whether git integration is enabled for showing git status.
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -322,28 +322,12 @@ pub struct GitSettings {
|
||||
pub path_style: Option<GitPathStyle>,
|
||||
}
|
||||
|
||||
/// Global controls for enabling or disabling git integration features.
|
||||
///
|
||||
/// These settings provide a centralized way to control git integration,
|
||||
/// rather than having to configure each feature individually.
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Master switch to disable all git integration features.
|
||||
/// When true, all git features are disabled regardless of other settings.
|
||||
///
|
||||
/// Default: false
|
||||
pub disable_git: Option<bool>,
|
||||
/// Whether to show git status indicators (modified, added, deleted)
|
||||
/// in the project panel, outline panel, and tabs.
|
||||
///
|
||||
/// Default: true
|
||||
pub enable_status: Option<bool>,
|
||||
/// Whether to show git diff information, including gutter diff
|
||||
/// indicators and scrollbar diff markers.
|
||||
///
|
||||
/// Default: true
|
||||
pub enable_diff: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -56,6 +56,12 @@ impl Label {
|
||||
pub fn set_text(&mut self, text: impl Into<SharedString>) {
|
||||
self.label = text.into();
|
||||
}
|
||||
|
||||
/// Truncates the label from the start, keeping the end visible.
|
||||
pub fn truncate_start(mut self) -> Self {
|
||||
self.base = self.base.truncate_start();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Style methods.
|
||||
@@ -256,7 +262,8 @@ impl Component for Label {
|
||||
"Special Cases",
|
||||
vec![
|
||||
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
|
||||
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
|
||||
single_example("Regular Truncation", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
|
||||
single_example("Start Truncation", div().max_w_24().child(Label::new("zed/crates/ui/src/components/label/truncate/label/label.rs").truncate_start()).into_any_element()),
|
||||
],
|
||||
),
|
||||
])
|
||||
|
||||
@@ -56,7 +56,7 @@ pub trait LabelCommon {
|
||||
/// Sets the alpha property of the label, overwriting the alpha value of the color.
|
||||
fn alpha(self, alpha: f32) -> Self;
|
||||
|
||||
/// Truncates overflowing text with an ellipsis (`…`) if needed.
|
||||
/// Truncates overflowing text with an ellipsis (`…`) at the end if needed.
|
||||
fn truncate(self) -> Self;
|
||||
|
||||
/// Sets the label to render as a single line.
|
||||
@@ -88,6 +88,7 @@ pub struct LabelLike {
|
||||
underline: bool,
|
||||
single_line: bool,
|
||||
truncate: bool,
|
||||
truncate_start: bool,
|
||||
}
|
||||
|
||||
impl Default for LabelLike {
|
||||
@@ -113,6 +114,7 @@ impl LabelLike {
|
||||
underline: false,
|
||||
single_line: false,
|
||||
truncate: false,
|
||||
truncate_start: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +128,12 @@ impl LabelLike {
|
||||
gpui::margin_style_methods!({
|
||||
visibility: pub
|
||||
});
|
||||
|
||||
/// Truncates overflowing text with an ellipsis (`…`) at the start if needed.
|
||||
pub fn truncate_start(mut self) -> Self {
|
||||
self.truncate_start = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelCommon for LabelLike {
|
||||
@@ -169,7 +177,7 @@ impl LabelCommon for LabelLike {
|
||||
self
|
||||
}
|
||||
|
||||
/// Truncates overflowing text with an ellipsis (`…`) if needed.
|
||||
/// Truncates overflowing text with an ellipsis (`…`) at the end if needed.
|
||||
fn truncate(mut self) -> Self {
|
||||
self.truncate = true;
|
||||
self
|
||||
@@ -235,6 +243,9 @@ impl RenderOnce for LabelLike {
|
||||
.when(self.truncate, |this| {
|
||||
this.overflow_x_hidden().text_ellipsis()
|
||||
})
|
||||
.when(self.truncate_start, |this| {
|
||||
this.overflow_x_hidden().text_ellipsis_start()
|
||||
})
|
||||
.text_color(color)
|
||||
.font_weight(
|
||||
self.weight
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
|
||||
});
|
||||
|
||||
/// Whether a given environment variable name should have its value redacted
|
||||
pub fn should_redact(env_var_name: &str) -> bool {
|
||||
const REDACTED_SUFFIXES: &[&str] = &[
|
||||
@@ -13,3 +19,31 @@ pub fn should_redact(env_var_name: &str) -> bool {
|
||||
.iter()
|
||||
.any(|suffix| env_var_name.ends_with(suffix))
|
||||
}
|
||||
|
||||
/// Redact a string which could include a command with environment variables
|
||||
pub fn redact_command(command: &str) -> String {
|
||||
REDACT_REGEX
|
||||
.replace_all(command, |caps: ®ex::Captures| {
|
||||
let var_name = &caps[1];
|
||||
let value = &caps[2];
|
||||
if should_redact(var_name) {
|
||||
format!(r#"{}="[REDACTED]""#, var_name)
|
||||
} else {
|
||||
format!("{}={}", var_name, value)
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_redact_string_with_multiple_env_vars() {
|
||||
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
|
||||
let result = redact_command(input);
|
||||
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3407,4 +3407,390 @@ mod test {
|
||||
.assert_eq(" ˇf = (x: unknown) => {");
|
||||
cx.shared_clipboard().await.assert_eq("const ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_typescript(cx).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const foo = () => {
|
||||
return ˇ1;
|
||||
};
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const foo = () => {
|
||||
return 1;
|
||||
};ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
arr.map(() => {
|
||||
return ˇ1;
|
||||
});
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
arr.map(«() => {
|
||||
return 1;
|
||||
}ˇ»);
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const foo = () => {
|
||||
return ˇ1;
|
||||
};
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v i f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
const foo = () => {
|
||||
«return 1;ˇ»
|
||||
};
|
||||
"},
|
||||
Mode::Visual,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
(() => {
|
||||
console.log(ˇ1);
|
||||
})();
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
(«() => {
|
||||
console.log(1);
|
||||
}ˇ»)();
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const foo = () => {
|
||||
return ˇ1;
|
||||
};
|
||||
export { foo };
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const foo = () => {
|
||||
return 1;
|
||||
};ˇ»
|
||||
export { foo };
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
let bar = () => {
|
||||
return ˇ2;
|
||||
};
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«let bar = () => {
|
||||
return 2;
|
||||
};ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
var baz = () => {
|
||||
return ˇ3;
|
||||
};
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«var baz = () => {
|
||||
return 3;
|
||||
};ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const add = (a, b) => a + ˇb;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const add = (a, b) => a + b;ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const add = ˇ(a, b) => a + b;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const add = (a, b) => a + b;ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const add = (a, b) => a + bˇ;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const add = (a, b) => a + b;ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
const add = (a, b) =ˇ> a + b;
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«const add = (a, b) => a + b;ˇ»
|
||||
"},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_tsx(cx).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => {
|
||||
alert("Hello world!");
|
||||
console.log(ˇ"clicked");
|
||||
}}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => {
|
||||
alert("Hello world!");
|
||||
console.log("clicked");
|
||||
}ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => console.log("clickˇed")}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => {
|
||||
console.log("cliˇcked");
|
||||
}}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => {
|
||||
console.log("clicked");
|
||||
}ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => fˇoo()}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("v a f");
|
||||
cx.assert_state(
|
||||
indoc! {r#"
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={«() => foo()ˇ»}>Hello world!</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"#},
|
||||
Mode::VisualLine,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,12 +522,16 @@ impl Vim {
|
||||
selection.start = original_point.to_display_point(map)
|
||||
}
|
||||
} else {
|
||||
selection.end = movement::saturating_right(
|
||||
map,
|
||||
original_point.to_display_point(map),
|
||||
);
|
||||
if original_point.column > 0 {
|
||||
selection.reversed = true
|
||||
let original_display_point =
|
||||
original_point.to_display_point(map);
|
||||
if selection.end <= original_display_point {
|
||||
selection.end = movement::saturating_right(
|
||||
map,
|
||||
original_display_point,
|
||||
);
|
||||
if original_point.column > 0 {
|
||||
selection.reversed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ use project::{
|
||||
};
|
||||
|
||||
use language::{LanguageName, Toolchain, ToolchainScope};
|
||||
use project::WorktreeId;
|
||||
use remote::{
|
||||
DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
|
||||
};
|
||||
@@ -845,6 +844,44 @@ impl Domain for WorkspaceDb {
|
||||
host_name TEXT
|
||||
) STRICT;
|
||||
),
|
||||
sql!(CREATE TABLE toolchains2 (
|
||||
workspace_id INTEGER,
|
||||
worktree_root_path TEXT NOT NULL,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
raw_json TEXT NOT NULL,
|
||||
relative_worktree_path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
|
||||
INSERT OR REPLACE INTO toolchains2
|
||||
// The `instr(paths, '\n') = 0` part allows us to find all
|
||||
// workspaces that have a single worktree, as `\n` is used as a
|
||||
// separator when serializing the workspace paths, so if no `\n` is
|
||||
// found, we know we have a single worktree.
|
||||
SELECT toolchains.workspace_id, paths, language_name, name, path, raw_json, relative_worktree_path FROM toolchains INNER JOIN workspaces ON toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
|
||||
DROP TABLE toolchains;
|
||||
ALTER TABLE toolchains2 RENAME TO toolchains;
|
||||
),
|
||||
sql!(CREATE TABLE user_toolchains2 (
|
||||
remote_connection_id INTEGER,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
worktree_root_path TEXT NOT NULL,
|
||||
relative_worktree_path TEXT NOT NULL,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
raw_json TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
|
||||
INSERT OR REPLACE INTO user_toolchains2
|
||||
// The `instr(paths, '\n') = 0` part allows us to find all
|
||||
// workspaces that have a single worktree, as `\n` is used as a
|
||||
// separator when serializing the workspace paths, so if no `\n` is
|
||||
// found, we know we have a single worktree.
|
||||
SELECT user_toolchains.remote_connection_id, user_toolchains.workspace_id, paths, relative_worktree_path, language_name, name, path, raw_json FROM user_toolchains INNER JOIN workspaces ON user_toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
|
||||
DROP TABLE user_toolchains;
|
||||
ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
|
||||
),
|
||||
];
|
||||
|
||||
// Allow recovering from bad migration that was initially shipped to nightly
|
||||
@@ -1030,11 +1067,11 @@ impl WorkspaceDb {
|
||||
workspace_id: WorkspaceId,
|
||||
remote_connection_id: Option<RemoteConnectionId>,
|
||||
) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
|
||||
type RowKind = (WorkspaceId, u64, String, String, String, String, String);
|
||||
type RowKind = (WorkspaceId, String, String, String, String, String, String);
|
||||
|
||||
let toolchains: Vec<RowKind> = self
|
||||
.select_bound(sql! {
|
||||
SELECT workspace_id, worktree_id, relative_worktree_path,
|
||||
SELECT workspace_id, worktree_root_path, relative_worktree_path,
|
||||
language_name, name, path, raw_json
|
||||
FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
|
||||
workspace_id IN (0, ?2)
|
||||
@@ -1048,7 +1085,7 @@ impl WorkspaceDb {
|
||||
|
||||
for (
|
||||
_workspace_id,
|
||||
worktree_id,
|
||||
worktree_root_path,
|
||||
relative_worktree_path,
|
||||
language_name,
|
||||
name,
|
||||
@@ -1058,22 +1095,24 @@ impl WorkspaceDb {
|
||||
{
|
||||
// INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
|
||||
let scope = if _workspace_id == WorkspaceId(0) {
|
||||
debug_assert_eq!(worktree_id, u64::MAX);
|
||||
debug_assert_eq!(worktree_root_path, String::default());
|
||||
debug_assert_eq!(relative_worktree_path, String::default());
|
||||
ToolchainScope::Global
|
||||
} else {
|
||||
debug_assert_eq!(workspace_id, _workspace_id);
|
||||
debug_assert_eq!(
|
||||
worktree_id == u64::MAX,
|
||||
worktree_root_path == String::default(),
|
||||
relative_worktree_path == String::default()
|
||||
);
|
||||
|
||||
let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
|
||||
continue;
|
||||
};
|
||||
if worktree_id != u64::MAX && relative_worktree_path != String::default() {
|
||||
if worktree_root_path != String::default()
|
||||
&& relative_worktree_path != String::default()
|
||||
{
|
||||
ToolchainScope::Subproject(
|
||||
WorktreeId::from_usize(worktree_id as usize),
|
||||
Arc::from(worktree_root_path.as_ref()),
|
||||
relative_path.into(),
|
||||
)
|
||||
} else {
|
||||
@@ -1159,13 +1198,13 @@ impl WorkspaceDb {
|
||||
|
||||
for (scope, toolchains) in workspace.user_toolchains {
|
||||
for toolchain in toolchains {
|
||||
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
|
||||
let (workspace_id, worktree_id, relative_worktree_path) = match scope {
|
||||
ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())),
|
||||
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
|
||||
let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
|
||||
ToolchainScope::Subproject(ref worktree_root_path, ref path) => (Some(workspace.id), Some(worktree_root_path.to_string_lossy().into_owned()), Some(path.as_unix_str().to_owned())),
|
||||
ToolchainScope::Project => (Some(workspace.id), None, None),
|
||||
ToolchainScope::Global => (None, None, None),
|
||||
};
|
||||
let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(),
|
||||
let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
|
||||
toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
|
||||
if let Err(err) = conn.exec_bound(query)?(args) {
|
||||
log::error!("{err}");
|
||||
@@ -1844,24 +1883,24 @@ impl WorkspaceDb {
|
||||
pub(crate) async fn toolchains(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
|
||||
) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
|
||||
self.write(move |this| {
|
||||
let mut select = this
|
||||
.select_bound(sql!(
|
||||
SELECT
|
||||
name, path, worktree_id, relative_worktree_path, language_name, raw_json
|
||||
name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
|
||||
FROM toolchains
|
||||
WHERE workspace_id = ?
|
||||
))
|
||||
.context("select toolchains")?;
|
||||
|
||||
let toolchain: Vec<(String, String, u64, String, String, String)> =
|
||||
let toolchain: Vec<(String, String, String, String, String, String)> =
|
||||
select(workspace_id)?;
|
||||
|
||||
Ok(toolchain
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, path, worktree_id, relative_worktree_path, language, json)| {
|
||||
|(name, path, worktree_root_path, relative_worktree_path, language, json)| {
|
||||
Some((
|
||||
Toolchain {
|
||||
name: name.into(),
|
||||
@@ -1869,7 +1908,7 @@ impl WorkspaceDb {
|
||||
language_name: LanguageName::new(&language),
|
||||
as_json: serde_json::Value::from_str(&json).ok()?,
|
||||
},
|
||||
WorktreeId::from_proto(worktree_id),
|
||||
Arc::from(worktree_root_path.as_ref()),
|
||||
RelPath::from_proto(&relative_worktree_path).log_err()?,
|
||||
))
|
||||
},
|
||||
@@ -1882,18 +1921,18 @@ impl WorkspaceDb {
|
||||
pub async fn set_toolchain(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_root_path: Arc<Path>,
|
||||
relative_worktree_path: Arc<RelPath>,
|
||||
toolchain: Toolchain,
|
||||
) -> Result<()> {
|
||||
log::debug!(
|
||||
"Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
|
||||
"Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
|
||||
toolchain.name
|
||||
);
|
||||
self.write(move |conn| {
|
||||
let mut insert = conn
|
||||
.exec_bound(sql!(
|
||||
INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
name = ?5,
|
||||
@@ -1904,7 +1943,7 @@ impl WorkspaceDb {
|
||||
|
||||
insert((
|
||||
workspace_id,
|
||||
worktree_id.to_usize(),
|
||||
worktree_root_path.to_string_lossy().into_owned(),
|
||||
relative_worktree_path.as_unix_str(),
|
||||
toolchain.language_name.as_ref(),
|
||||
toolchain.name.as_ref(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user